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}