alpm_types/relation/
soname.rs

1//! Representation of [soname] information in [ELF] files.
2//!
3//! [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
4//! [soname]: https://en.wikipedia.org/wiki/Soname
5
6use std::{
7    fmt::{Display, Formatter},
8    str::FromStr,
9};
10
11use serde::{Deserialize, Serialize};
12use winnow::{
13    ModalResult,
14    Parser,
15    ascii::digit1,
16    combinator::{alt, cut_err, eof, fail, peek, repeat, repeat_till},
17    error::{StrContext, StrContextValue},
18    stream::Stream,
19    token::{any, rest, take_while},
20};
21
22#[cfg(doc)]
23use crate::PackageRelation;
24use crate::{ElfArchitectureFormat, Error, Name, PackageVersion, SharedObjectName};
25
26/// Provides either a [`PackageVersion`] or a [`SharedObjectName`].
27///
28/// This enum is used when creating [`SonameV1`].
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub enum VersionOrSoname {
31    /// A version for a [`SonameV1`].
32    Version(PackageVersion),
33
34    /// A soname for a [`SonameV1`].
35    Soname(SharedObjectName),
36}
37
38impl FromStr for VersionOrSoname {
39    type Err = Error;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        Ok(Self::parser.parse(s)?)
43    }
44}
45
46impl VersionOrSoname {
47    /// Recognizes a [`PackageVersion`] or [`SharedObjectName`] in a string slice.
48    ///
49    /// First attempts to recognize a [`SharedObjectName`] and if that fails, falls back to
50    /// recognizing a [`PackageVersion`].
51    pub fn parser(input: &mut &str) -> ModalResult<Self> {
52        // In the following, we're doing our own `alt` implementation.
53        // The reason for this is that we build our type parsers so that they throw errors
54        // if they encounter unexpected input instead of backtracking.
55        let checkpoint = input.checkpoint();
56        let soname_result = SharedObjectName::parser.parse_next(input);
57        if soname_result.is_ok() {
58            let soname = soname_result?;
59            return Ok(VersionOrSoname::Soname(soname));
60        }
61
62        input.reset(&checkpoint);
63        let version_result = rest.and_then(PackageVersion::parser).parse_next(input);
64        if version_result.is_ok() {
65            let version = version_result?;
66            return Ok(VersionOrSoname::Version(version));
67        }
68
69        cut_err(fail)
70            .context(StrContext::Expected(StrContextValue::Description(
71                "version or shared object name",
72            )))
73            .parse_next(input)
74    }
75}
76
77impl Display for VersionOrSoname {
78    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79        match self {
80            VersionOrSoname::Version(version) => write!(f, "{version}"),
81            VersionOrSoname::Soname(soname) => write!(f, "{soname}"),
82        }
83    }
84}
85
86/// Representation of [soname] data of a shared object based on the [alpm-sonamev1] specification.
87///
88/// Soname data may be used as [alpm-package-relation] of type _provision_ and _run-time
89/// dependency_.
90/// This type distinguishes between three forms: _basic_, _unversioned_ and _explicit_.
91///
92/// - [`SonameV1::Basic`] is used when only the `name` of a _shared object_ file is used. This form
93///   can be used in files that may contain static data about package sources (e.g. [PKGBUILD] or
94///   [SRCINFO] files).
95/// - [`SonameV1::Unversioned`] is used when the `name` of a _shared object_ file, its _soname_
96///   (which does _not_ expose a specific version) and its `architecture` (derived from the [ELF]
97///   class of the file) are used. This form can be used in files that may contain dynamic data
98///   derived from a specific package build environment (i.e. [PKGINFO]). It is discouraged to use
99///   this form in files that contain static data about package sources (e.g. [PKGBUILD] or
100///   [SRCINFO] files).
101/// - [`SonameV1::Explicit`] is used when the `name` of a _shared object_ file, the `version` from
102///   its _soname_ and its `architecture` (derived from the [ELF] class of the file) are used. This
103///   form can be used in files that may contain dynamic data derived from a specific package build
104///   environment (i.e. [PKGINFO]). It is discouraged to use this form in files that contain static
105///   data about package sources (e.g. [PKGBUILD] or [SRCINFO] files).
106///
107/// # Warning
108///
109/// This type is **deprecated** and `SonameV2` should be preferred instead!
110/// Due to the loose nature of the [alpm-sonamev1] specification, the _basic_ form overlaps with the
111/// specification of [`Name`] and the _explicit_ form overlaps with that of [`PackageRelation`].
112///
113/// # Examples
114///
115/// ```
116/// use alpm_types::{ElfArchitectureFormat, SonameV1};
117///
118/// # fn main() -> Result<(), alpm_types::Error> {
119/// let basic_soname = SonameV1::Basic("example.so".parse()?);
120/// let unversioned_soname = SonameV1::Unversioned {
121///     name: "example.so".parse()?,
122///     soname: "example.so".parse()?,
123///     architecture: ElfArchitectureFormat::Bit64,
124/// };
125/// let explicit_soname = SonameV1::Explicit {
126///     name: "example.so".parse()?,
127///     version: "1.0.0".parse()?,
128///     architecture: ElfArchitectureFormat::Bit64,
129/// };
130/// # Ok(())
131/// # }
132/// ```
133///
134/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
135/// [alpm-sonamev1]: https://alpm.archlinux.page/specifications/alpm-sonamev1.7.html
136/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
137/// [soname]: https://en.wikipedia.org/wiki/Soname
138/// [PKGBUILD]: https://man.archlinux.org/man/PKGBUILD.5
139/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
140/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
141#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
142pub enum SonameV1 {
143    /// Basic representation of a _shared object_ file.
144    ///
145    /// Tracks the `name` of a _shared object_ file.
146    /// This form is used when referring to _shared object_ files without their soname data.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use std::str::FromStr;
152    ///
153    /// use alpm_types::SonameV1;
154    ///
155    /// # fn main() -> Result<(), alpm_types::Error> {
156    /// let soname = SonameV1::from_str("example.so")?;
157    /// assert_eq!(soname, SonameV1::Basic("example.so".parse()?));
158    /// # Ok(())
159    /// # }
160    /// ```
161    Basic(SharedObjectName),
162
163    /// Unversioned representation of an ELF file's soname data.
164    ///
165    /// Tracks the `name` of a _shared object_ file, its _soname_ instead of a version and its
166    /// `architecture`. This form is used if the _soname data_ of a _shared object_ does not
167    /// expose a version.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use std::str::FromStr;
173    ///
174    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
175    ///
176    /// # fn main() -> Result<(), alpm_types::Error> {
177    /// let soname = SonameV1::from_str("example.so=example.so-64")?;
178    /// assert_eq!(
179    ///     soname,
180    ///     SonameV1::Unversioned {
181    ///         name: "example.so".parse()?,
182    ///         soname: "example.so".parse()?,
183    ///         architecture: ElfArchitectureFormat::Bit64,
184    ///     }
185    /// );
186    /// # Ok(())
187    /// # }
188    /// ```
189    Unversioned {
190        /// The least specific name of the shared object file.
191        name: SharedObjectName,
192        /// The value of the shared object's _SONAME_ field in its _dynamic section_.
193        soname: SharedObjectName,
194        /// The ELF architecture format of the shared object file.
195        architecture: ElfArchitectureFormat,
196    },
197
198    /// Explicit representation of an ELF file's soname data.
199    ///
200    /// Tracks the `name` of a _shared object_ file, the `version` of its _soname_ and its
201    /// `architecture`. This form is used if the _soname data_ of a _shared object_ exposes a
202    /// specific version.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use std::str::FromStr;
208    ///
209    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
210    ///
211    /// # fn main() -> Result<(), alpm_types::Error> {
212    /// let soname = SonameV1::from_str("example.so=1.0.0-64")?;
213    /// assert_eq!(
214    ///    soname,
215    ///    SonameV1::Explicit {
216    ///         name: "example.so".parse()?,
217    ///         version: "1.0.0".parse()?,
218    ///         architecture: ElfArchitectureFormat::Bit64,
219    ///     }
220    /// );
221    /// # Ok(())
222    /// # }
223    Explicit {
224        /// The least specific name of the shared object file.
225        name: SharedObjectName,
226        /// The version of the shared object file (as exposed in its _soname_ data).
227        version: PackageVersion,
228        /// The ELF architecture format of the shared object file.
229        architecture: ElfArchitectureFormat,
230    },
231}
232
233impl SonameV1 {
234    /// Creates a new [`SonameV1`].
235    ///
236    /// Depending on input, this function returns different variants of [`SonameV1`]:
237    ///
238    /// - [`SonameV1::Basic`], if both `version_or_soname` and `architecture` are [`None`]
239    /// - [`SonameV1::Unversioned`], if `version_or_soname` is [`VersionOrSoname::Soname`] and
240    ///   `architecture` is [`Some`]
241    /// - [`SonameV1::Explicit`], if `version_or_soname` is [`VersionOrSoname::Version`] and
242    ///   `architecture` is [`Some`]
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
248    ///
249    /// # fn main() -> Result<(), alpm_types::Error> {
250    /// let basic_soname = SonameV1::new("example.so".parse()?, None, None)?;
251    /// assert_eq!(basic_soname, SonameV1::Basic("example.so".parse()?));
252    ///
253    /// let unversioned_soname = SonameV1::new(
254    ///     "example.so".parse()?,
255    ///     Some("example.so".parse()?),
256    ///     Some(ElfArchitectureFormat::Bit64),
257    /// )?;
258    /// assert_eq!(
259    ///     unversioned_soname,
260    ///     SonameV1::Unversioned {
261    ///         name: "example.so".parse()?,
262    ///         soname: "example.so".parse()?,
263    ///         architecture: "64".parse()?
264    ///     }
265    /// );
266    ///
267    /// let explicit_soname = SonameV1::new(
268    ///     "example.so".parse()?,
269    ///     Some("1.0.0".parse()?),
270    ///     Some(ElfArchitectureFormat::Bit64),
271    /// )?;
272    /// assert_eq!(
273    ///     explicit_soname,
274    ///     SonameV1::Explicit {
275    ///         name: "example.so".parse()?,
276    ///         version: "1.0.0".parse()?,
277    ///         architecture: "64".parse()?
278    ///     }
279    /// );
280    /// # Ok(())
281    /// # }
282    /// ```
283    pub fn new(
284        name: SharedObjectName,
285        version_or_soname: Option<VersionOrSoname>,
286        architecture: Option<ElfArchitectureFormat>,
287    ) -> Result<Self, Error> {
288        match (version_or_soname, architecture) {
289            (None, None) => Ok(Self::Basic(name)),
290            (Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
291                name,
292                version,
293                architecture,
294            }),
295            (Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
296                name,
297                soname,
298                architecture,
299            }),
300            (None, Some(_)) => Err(Error::InvalidSonameV1(
301                "SonameV1 needs a version when specifying architecture",
302            )),
303            (Some(_), None) => Err(Error::InvalidSonameV1(
304                "SonameV1 needs an architecture when specifying version",
305            )),
306        }
307    }
308
309    /// Parses a [`SonameV1`] from a string slice.
310    pub fn parser(input: &mut &str) -> ModalResult<Self> {
311        // Parse the shared object name.
312        let name = Self::parse_shared_object_name(input)?;
313
314        // Parse the version delimiter `=`.
315        //
316        // If it doesn't exist, it is the basic form.
317        if Self::parse_version_delimiter(input).is_err() {
318            return Ok(SonameV1::Basic(name));
319        }
320
321        // Take all input until we hit the delimiter and architecture.
322        let (raw_version_or_soname, _): (String, _) =
323            cut_err(repeat_till(1.., any, peek(("-", digit1, eof))))
324                .context(StrContext::Expected(StrContextValue::Description(
325                    "a version or shared object name, followed by an ELF architecture format",
326                )))
327                .parse_next(input)?;
328
329        // Two cases are possible here:
330        //
331        // 1. Unversioned: `name=soname-architecture`
332        // 2. Explicit: `name=version-architecture`
333        let version_or_soname =
334            VersionOrSoname::parser.parse_next(&mut raw_version_or_soname.as_str())?;
335
336        // Parse the `-` delimiter
337        Self::parse_architecture_delimiter(input)?;
338
339        // Parse the architecture
340        let architecture = Self::parse_architecture(input)?;
341
342        match version_or_soname {
343            VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
344                name,
345                version,
346                architecture,
347            }),
348            VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
349                name,
350                soname,
351                architecture,
352            }),
353        }
354    }
355
356    /// Parses the shared object name until the version delimiter `=`.
357    fn parse_shared_object_name(input: &mut &str) -> ModalResult<SharedObjectName> {
358        repeat_till(1.., any, peek(alt(("=", eof))))
359            .try_map(|(name, _): (String, &str)| SharedObjectName::from_str(&name))
360            .context(StrContext::Label("shared object name"))
361            .parse_next(input)
362    }
363
364    /// Parses the version delimiter `=`.
365    ///
366    /// This function discards the result for only checking if the version delimiter is present.
367    fn parse_version_delimiter(input: &mut &str) -> ModalResult<()> {
368        cut_err("=")
369            .context(StrContext::Label("version delimiter"))
370            .context(StrContext::Expected(StrContextValue::Description(
371                "version delimiter `=`",
372            )))
373            .parse_next(input)
374            .map(|_| ())
375    }
376
377    /// Parses the architecture delimiter `-`.
378    fn parse_architecture_delimiter(input: &mut &str) -> ModalResult<()> {
379        cut_err("-")
380            .context(StrContext::Label("architecture delimiter"))
381            .context(StrContext::Expected(StrContextValue::Description(
382                "architecture delimiter `-`",
383            )))
384            .parse_next(input)
385            .map(|_| ())
386    }
387
388    /// Parses the architecture.
389    fn parse_architecture(input: &mut &str) -> ModalResult<ElfArchitectureFormat> {
390        cut_err(take_while(1.., |c: char| c.is_ascii_digit()))
391            .try_map(ElfArchitectureFormat::from_str)
392            .context(StrContext::Label("architecture"))
393            .parse_next(input)
394    }
395
396    /// Returns a reference to the [`SharedObjectName`] of the [`SonameV1`].
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use alpm_types::{ElfArchitectureFormat, SharedObjectName, SonameV1};
402    ///
403    /// # fn main() -> Result<(), alpm_types::Error> {
404    /// let shared_object_name: SharedObjectName = "example.so".parse()?;
405    ///
406    /// let basic = SonameV1::new("example.so".parse()?, None, None)?;
407    /// assert_eq!(&shared_object_name, basic.shared_object_name());
408    ///
409    /// let unversioned = SonameV1::new(
410    ///     "example.so".parse()?,
411    ///     Some("example.so".parse()?),
412    ///     Some(ElfArchitectureFormat::Bit64),
413    /// )?;
414    /// assert_eq!(&shared_object_name, unversioned.shared_object_name());
415    ///
416    /// let explicit = SonameV1::new(
417    ///     "example.so".parse()?,
418    ///     Some("1.0.0".parse()?),
419    ///     Some(ElfArchitectureFormat::Bit64),
420    /// )?;
421    /// assert_eq!(&shared_object_name, explicit.shared_object_name());
422    /// # Ok(())
423    /// # }
424    /// ```
425    pub fn shared_object_name(&self) -> &SharedObjectName {
426        match self {
427            SonameV1::Basic(name) => name,
428            SonameV1::Unversioned { name, .. } => name,
429            SonameV1::Explicit { name, .. } => name,
430        }
431    }
432}
433
434impl FromStr for SonameV1 {
435    type Err = Error;
436    /// Parses a [`SonameV1`] from a string slice.
437    ///
438    /// The string slice must be in the format `name[=version-architecture]`.
439    ///
440    /// # Errors
441    ///
442    /// Returns an error if a [`SonameV1`] can not be parsed from input.
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use std::str::FromStr;
448    ///
449    /// use alpm_types::{ElfArchitectureFormat, SonameV1};
450    ///
451    /// # fn main() -> Result<(), alpm_types::Error> {
452    /// assert_eq!(
453    ///     SonameV1::from_str("example.so=1.0.0-64")?,
454    ///     SonameV1::Explicit {
455    ///         name: "example.so".parse()?,
456    ///         version: "1.0.0".parse()?,
457    ///         architecture: ElfArchitectureFormat::Bit64,
458    ///     },
459    /// );
460    /// assert_eq!(
461    ///     SonameV1::from_str("example.so=example.so-64")?,
462    ///     SonameV1::Unversioned {
463    ///         name: "example.so".parse()?,
464    ///         soname: "example.so".parse()?,
465    ///         architecture: ElfArchitectureFormat::Bit64,
466    ///     },
467    /// );
468    /// assert_eq!(
469    ///     SonameV1::from_str("example.so")?,
470    ///     SonameV1::Basic("example.so".parse()?),
471    /// );
472    /// # Ok(())
473    /// # }
474    /// ```
475    fn from_str(s: &str) -> Result<Self, Self::Err> {
476        Ok(Self::parser.parse(s)?)
477    }
478}
479
480impl Display for SonameV1 {
481    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
482        match self {
483            Self::Basic(name) => write!(f, "{name}"),
484            Self::Unversioned {
485                name,
486                soname,
487                architecture,
488            } => write!(f, "{name}={soname}-{architecture}"),
489            Self::Explicit {
490                name,
491                version,
492                architecture,
493            } => write!(f, "{name}={version}-{architecture}"),
494        }
495    }
496}
497
498/// A prefix associated with a library lookup directory.
499///
500/// Library lookup directories are used when detecting shared object files on a system.
501/// Each such lookup directory can be assigned to a _prefix_, which allows identifying them in other
502/// contexts. E.g. `lib` may serve as _prefix_ for the lookup directory `/usr/lib`.
503///
504/// This is a type alias for [`Name`].
505pub type SharedLibraryPrefix = Name;
506
507/// The value of a shared object's _soname_.
508///
509/// This data may be present in the _SONAME_ or _NEEDED_ fields of a shared object's _dynamic
510/// section_.
511///
512/// The _soname_ data may contain only a shared object name (e.g. `libexample.so`) or a shared
513/// object name, that also encodes version information (e.g. `libexample.so.1`).
514#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
515pub struct Soname {
516    /// The name part of a shared object's _soname_.
517    pub name: SharedObjectName,
518    /// The optional version part of a shared object's _soname_.
519    pub version: Option<PackageVersion>,
520}
521
522impl Soname {
523    /// Creates a new [`Soname`].
524    pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
525        Self { name, version }
526    }
527
528    /// Recognizes a [`Soname`] in a string slice.
529    ///
530    /// The passed data can be in the following formats:
531    ///
532    /// - `<name>.so`: A shared object name without a version. (e.g. `libexample.so`)
533    /// - `<name>.so.<version>`: A shared object name with a version. (e.g. `libexample.so.1`)
534    ///     - The version must be a valid [`PackageVersion`].
535    pub fn parser(input: &mut &str) -> ModalResult<Self> {
536        let name = cut_err(
537            (
538                // Parse the name of the shared object until eof or the `.so` is hit.
539                repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof)))),
540                // Parse at least one or more `.so` suffix(es).
541                cut_err(repeat::<_, _, String, _, _>(1.., ".so"))
542                    .context(StrContext::Label("suffix"))
543                    .context(StrContext::Expected(StrContextValue::Description(
544                        "shared object name suffix '.so'",
545                    ))),
546            )
547                // Take both parts and map them onto a SharedObjectName
548                .take()
549                .and_then(Name::parser)
550                .map(SharedObjectName),
551        )
552        .context(StrContext::Label("shared object name"))
553        .parse_next(input)?;
554
555        // Parse the version delimiter.
556        let delimiter = cut_err(alt((".", eof)))
557            .context(StrContext::Label("version delimiter"))
558            .context(StrContext::Expected(StrContextValue::Description(
559                "version delimiter `.`",
560            )))
561            .parse_next(input)?;
562
563        // If a `.` is found, map the rest of the string to a version.
564        // Otherwise, we hit the `eof` and there's no version.
565        let version = match delimiter {
566            "" => None,
567            "." => Some(rest.and_then(PackageVersion::parser).parse_next(input)?),
568            _ => unreachable!(),
569        };
570
571        Ok(Self { name, version })
572    }
573}
574
575impl Display for Soname {
576    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
577        match &self.version {
578            Some(version) => write!(f, "{name}.{version}", name = self.name),
579            None => write!(f, "{name}", name = self.name),
580        }
581    }
582}
583
584impl FromStr for Soname {
585    type Err = Error;
586
587    /// Recognizes a [`Soname`] in a string slice.
588    ///
589    /// The string slice must be in the format of `<name>.so` or `<name>.so.<version>`.
590    ///
591    /// # Errors
592    ///
593    /// Returns an error if a [`Soname`] can not be parsed from input.
594    ///
595    /// # Examples
596    ///
597    /// ```
598    /// use std::str::FromStr;
599    ///
600    /// use alpm_types::Soname;
601    /// # fn main() -> Result<(), alpm_types::Error> {
602    /// assert_eq!(
603    ///     Soname::from_str("libexample.so.1")?,
604    ///     Soname::new("libexample.so".parse()?, Some("1".parse()?)),
605    /// );
606    /// assert_eq!(
607    ///     Soname::from_str("libexample.so")?,
608    ///     Soname::new("libexample.so".parse()?, None),
609    /// );
610    /// # Ok(())
611    /// # }
612    /// ```
613    fn from_str(s: &str) -> Result<Self, Self::Err> {
614        Ok(Self::parser.parse(s)?)
615    }
616}
617
618/// Representation of [soname] data of a shared object based on the [alpm-sonamev2] specification.
619///
620/// Soname data may be used as [alpm-package-relation] of type _provision_ or _run-time dependency_
621/// in [`PackageInfoV1`] and [`PackageInfoV2`]. The data consists of the arbitrarily
622/// defined `prefix`, which denotes the use name of a specific library directory, and the `soname`,
623/// which refers to the value of either the _SONAME_ or a _NEEDED_ field in the _dynamic section_ of
624/// an [ELF] file.
625///
626/// # Examples
627///
628/// This example assumpes that `lib` is used as the `prefix` for the library directory `/usr/lib`
629/// and the following files are contained in it:
630///
631/// ```bash
632/// /usr/lib/libexample.so -> libexample.so.1
633/// /usr/lib/libexample.so.1 -> libexample.so.1.0.0
634/// /usr/lib/libexample.so.1.0.0
635/// ```
636///
637/// The above file `/usr/lib/libexample.so.1.0.0` represents an [ELF] file, that exposes
638/// `libexample.so.1` as value of the _SONAME_ field in its _dynamic section_. This data can be
639/// represented as follows, using [`SonameV2`]:
640///
641/// ```rust
642/// use alpm_types::{Soname, SonameV2};
643///
644/// # fn main() -> Result<(), alpm_types::Error> {
645/// let soname_data = SonameV2 {
646///     prefix: "lib".parse()?,
647///     soname: Soname {
648///         name: "libexample.so".parse()?,
649///         version: Some("1".parse()?),
650///     },
651/// };
652/// assert_eq!(soname_data.to_string(), "lib:libexample.so.1");
653/// # Ok(())
654/// # }
655/// ```
656///
657/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
658/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
659/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
660/// [soname]: https://en.wikipedia.org/wiki/Soname
661/// [`PackageInfoV1`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV1.html
662/// [`PackageInfoV2`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV2.html
663#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
664pub struct SonameV2 {
665    /// The directory prefix of the shared object file.
666    pub prefix: SharedLibraryPrefix,
667    /// The _soname_ of a shared object file.
668    pub soname: Soname,
669}
670
671impl SonameV2 {
672    /// Creates a new [`SonameV2`].
673    ///
674    /// # Examples
675    ///
676    /// ```
677    /// use alpm_types::SonameV2;
678    ///
679    /// # fn main() -> Result<(), alpm_types::Error> {
680    /// SonameV2::new("lib".parse()?, "libexample.so.1".parse()?);
681    /// # Ok(())
682    /// # }
683    /// ```
684    pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
685        Self { prefix, soname }
686    }
687
688    /// Recognizes a [`SonameV2`] in a string slice.
689    ///
690    /// The passed data must be in the format `<prefix>:<soname>`. (e.g. `lib:libexample.so.1`)
691    ///
692    /// See [`Soname::parser`] for details on the format of `<soname>`.
693    ///
694    /// # Errors
695    ///
696    /// Returns an error if no [`SonameV2`] can be created from `input`.
697    pub fn parser(input: &mut &str) -> ModalResult<Self> {
698        // Parse everything from the start to the first `:` and parse as `SharedLibraryPrefix`.
699        let prefix = cut_err(
700            repeat_till(1.., any, peek(alt((":", eof))))
701                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
702        )
703        .context(StrContext::Label("prefix for a shared object lookup path"))
704        .parse_next(input)?;
705
706        cut_err(":")
707            .context(StrContext::Label("shared library prefix delimiter"))
708            .context(StrContext::Expected(StrContextValue::Description(
709                "shared library prefix `:`",
710            )))
711            .parse_next(input)?;
712
713        let soname = Soname::parser.parse_next(input)?;
714
715        Ok(Self { prefix, soname })
716    }
717}
718
719impl FromStr for SonameV2 {
720    type Err = Error;
721
722    /// Parses a [`SonameV2`] from a string slice.
723    ///
724    /// The string slice must be in the format `<prefix>:<soname>`.
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if a [`SonameV2`] can not be parsed from input.
729    ///
730    /// # Examples
731    ///
732    /// ```
733    /// use std::str::FromStr;
734    ///
735    /// use alpm_types::{Soname, SonameV2};
736    ///
737    /// # fn main() -> Result<(), alpm_types::Error> {
738    /// assert_eq!(
739    ///     SonameV2::from_str("lib:libexample.so.1")?,
740    ///     SonameV2::new(
741    ///         "lib".parse()?,
742    ///         Soname::new("libexample.so".parse()?, Some("1".parse()?))
743    ///     ),
744    /// );
745    /// # Ok(())
746    /// # }
747    /// ```
748    fn from_str(s: &str) -> Result<Self, Self::Err> {
749        Ok(Self::parser.parse(s)?)
750    }
751}
752
753impl Display for SonameV2 {
754    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
755        write!(
756            f,
757            "{prefix}:{soname}",
758            prefix = self.prefix,
759            soname = self.soname
760        )
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use rstest::rstest;
767
768    use super::*;
769
770    #[rstest]
771    #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
772    #[case("example.so=1.0.0-64", SonameV1::Explicit {
773        name: "example.so".parse().unwrap(),
774        version: "1.0.0".parse().unwrap(),
775        architecture: ElfArchitectureFormat::Bit64,
776    })]
777    fn sonamev1_from_string(
778        #[case] input: &str,
779        #[case] expected_result: SonameV1,
780    ) -> testresult::TestResult<()> {
781        let soname = SonameV1::from_str(input)?;
782        assert_eq!(expected_result, soname);
783        assert_eq!(input, soname.to_string());
784        Ok(())
785    }
786
787    #[rstest]
788    #[case(
789        "libwlroots-0.18.so=libwlroots-0.18.so-64",
790        SonameV1::Unversioned {
791            name: "libwlroots-0.18.so".parse().unwrap(),
792            soname: "libwlroots-0.18.so".parse().unwrap(),
793            architecture: ElfArchitectureFormat::Bit64,
794        },
795    )]
796    #[case(
797        "libexample.so=otherlibexample.so-64",
798        SonameV1::Unversioned {
799            name: "libexample.so".parse().unwrap(),
800            soname: "otherlibexample.so".parse().unwrap(),
801            architecture: ElfArchitectureFormat::Bit64,
802        },
803    )]
804    fn sonamev1_from_string_without_version(
805        #[case] input: &str,
806        #[case] expected_result: SonameV1,
807    ) -> testresult::TestResult<()> {
808        let soname = SonameV1::from_str(input)?;
809        assert_eq!(expected_result, soname);
810        assert_eq!(input, soname.to_string());
811        Ok(())
812    }
813
814    #[rstest]
815    #[case("noso", "invalid shared object name")]
816    #[case("invalidversion.so=1🐀2-64", "expected version or shared object name")]
817    #[case(
818        "nodelimiter.so=1.64",
819        "expected a version or shared object name, followed by an ELF architecture format"
820    )]
821    #[case(
822        "noarchitecture.so=1-",
823        "expected a version or shared object name, followed by an ELF architecture format"
824    )]
825    #[case("invalidarchitecture.so=1-82", "invalid architecture")]
826    #[case("invalidsoname.so~1.64", "unexpected trailing content")]
827    fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
828        let result = SonameV1::from_str(input);
829        assert!(result.is_err(), "Expected SonameV1 parsing to fail");
830        let err = result.unwrap_err();
831        let pretty_error = err.to_string();
832        assert!(
833            pretty_error.contains(error_snippet),
834            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
835        );
836    }
837
838    #[rstest]
839    #[case(
840        "otherlibexample.so",
841        VersionOrSoname::Soname(
842            SharedObjectName::new("otherlibexample.so").unwrap())
843    )]
844    #[case(
845        "1.0.0",
846        VersionOrSoname::Version(
847            PackageVersion::from_str("1.0.0").unwrap())
848    )]
849    fn version_or_soname_from_string(
850        #[case] input: &str,
851        #[case] expected_result: VersionOrSoname,
852    ) -> testresult::TestResult<()> {
853        let version = VersionOrSoname::from_str(input)?;
854        assert_eq!(expected_result, version);
855        assert_eq!(input, version.to_string());
856        Ok(())
857    }
858
859    #[rstest]
860    #[case(
861        "lib:libexample.so",
862        SonameV2 {
863            prefix: "lib".parse().unwrap(),
864            soname: Soname {
865                name: "libexample.so".parse().unwrap(),
866                version: None,
867            },
868        },
869    )]
870    #[case(
871        "usr:libexample.so.1",
872        SonameV2 {
873            prefix: "usr".parse().unwrap(),
874            soname: Soname {
875                name: "libexample.so".parse().unwrap(),
876                version: "1".parse().ok(),
877            },
878        },
879    )]
880    #[case(
881        "lib:libexample.so.1.2.3",
882        SonameV2 {
883            prefix: "lib".parse().unwrap(),
884            soname: Soname {
885                name: "libexample.so".parse().unwrap(),
886                version: "1.2.3".parse().ok(),
887            },
888        },
889    )]
890    #[case(
891        "lib:libexample.so.so.420",
892        SonameV2 {
893            prefix: "lib".parse().unwrap(),
894            soname: Soname {
895                name: "libexample.so.so".parse().unwrap(),
896                version: "420".parse().ok(),
897            },
898        },
899    )]
900    #[case(
901        "lib:libexample.so.test",
902        SonameV2 {
903            prefix: "lib".parse().unwrap(),
904            soname: Soname {
905                name: "libexample.so".parse().unwrap(),
906                version: "test".parse().ok(),
907            },
908        },
909    )]
910    fn sonamev2_from_string(
911        #[case] input: &str,
912        #[case] expected_result: SonameV2,
913    ) -> testresult::TestResult<()> {
914        let soname = SonameV2::from_str(input)?;
915        assert_eq!(expected_result, soname);
916        assert_eq!(input, soname.to_string());
917        Ok(())
918    }
919
920    #[rstest]
921    #[case("libexample.so.1", "invalid shared library prefix delimiter")]
922    #[case("lib:libexample.so-abc", "invalid version delimiter")]
923    #[case("lib:libexample.so.10-10", "invalid pkgver character")]
924    #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
925    fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
926        let result = SonameV2::from_str(input);
927        assert!(result.is_err(), "Expected SonameV2 parsing to fail");
928        let err = result.unwrap_err();
929        let pretty_error = err.to_string();
930        assert!(
931            pretty_error.contains(error_snippet),
932            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
933        );
934    }
935}