Skip to main content

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