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