alpm_types/relation.rs
1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use serde::{Deserialize, Serialize};
7use winnow::{
8 ModalResult,
9 Parser,
10 ascii::digit1,
11 combinator::{
12 alt,
13 cut_err,
14 eof,
15 fail,
16 opt,
17 peek,
18 repeat,
19 repeat_till,
20 separated_pair,
21 seq,
22 terminated,
23 },
24 error::{StrContext, StrContextValue},
25 stream::Stream,
26 token::{any, rest, take_till, take_until, take_while},
27};
28
29use crate::{
30 ElfArchitectureFormat,
31 Error,
32 Name,
33 PackageVersion,
34 SharedObjectName,
35 VersionRequirement,
36};
37
38/// Provides either a [`PackageVersion`] or a [`SharedObjectName`].
39///
40/// This enum is used when creating [`SonameV1`].
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum VersionOrSoname {
43 /// A version for a [`SonameV1`].
44 Version(PackageVersion),
45
46 /// A soname for a [`SonameV1`].
47 Soname(SharedObjectName),
48}
49
50impl FromStr for VersionOrSoname {
51 type Err = Error;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 Ok(Self::parser.parse(s)?)
55 }
56}
57
58impl VersionOrSoname {
59 /// Recognizes a [`PackageVersion`] or [`SharedObjectName`] in a string slice.
60 ///
61 /// First attempts to recognize a [`SharedObjectName`] and if that fails, falls back to
62 /// recognizing a [`PackageVersion`].
63 pub fn parser(input: &mut &str) -> ModalResult<Self> {
64 // In the following, we're doing our own `alt` implementation.
65 // The reason for this is that we build our type parsers so that they throw errors
66 // if they encounter unexpected input instead of backtracking.
67 let checkpoint = input.checkpoint();
68 let soname_result = SharedObjectName::parser.parse_next(input);
69 if soname_result.is_ok() {
70 let soname = soname_result?;
71 return Ok(VersionOrSoname::Soname(soname));
72 }
73
74 input.reset(&checkpoint);
75 let version_result = rest.and_then(PackageVersion::parser).parse_next(input);
76 if version_result.is_ok() {
77 let version = version_result?;
78 return Ok(VersionOrSoname::Version(version));
79 }
80
81 cut_err(fail)
82 .context(StrContext::Expected(StrContextValue::Description(
83 "version or shared object name",
84 )))
85 .parse_next(input)
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, Deserialize, Eq, PartialEq, Serialize)]
154pub enum SonameV1 {
155 /// Basic representation of a _shared object_ file.
156 ///
157 /// Tracks the `name` of a _shared object_ file.
158 /// This form is used when referring to _shared object_ files without their soname data.
159 ///
160 /// # Examples
161 ///
162 /// ```
163 /// use std::str::FromStr;
164 ///
165 /// use alpm_types::SonameV1;
166 ///
167 /// # fn main() -> Result<(), alpm_types::Error> {
168 /// let soname = SonameV1::from_str("example.so")?;
169 /// assert_eq!(soname, SonameV1::Basic("example.so".parse()?));
170 /// # Ok(())
171 /// # }
172 /// ```
173 Basic(SharedObjectName),
174
175 /// Unversioned representation of an ELF file's soname data.
176 ///
177 /// Tracks the `name` of a _shared object_ file, its _soname_ instead of a version and its
178 /// `architecture`. This form is used if the _soname data_ of a _shared object_ does not
179 /// expose a version.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use std::str::FromStr;
185 ///
186 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
187 ///
188 /// # fn main() -> Result<(), alpm_types::Error> {
189 /// let soname = SonameV1::from_str("example.so=example.so-64")?;
190 /// assert_eq!(
191 /// soname,
192 /// SonameV1::Unversioned {
193 /// name: "example.so".parse()?,
194 /// soname: "example.so".parse()?,
195 /// architecture: ElfArchitectureFormat::Bit64,
196 /// }
197 /// );
198 /// # Ok(())
199 /// # }
200 /// ```
201 Unversioned {
202 /// The least specific name of the shared object file.
203 name: SharedObjectName,
204 /// The value of the shared object's _SONAME_ field in its _dynamic section_.
205 soname: SharedObjectName,
206 /// The ELF architecture format of the shared object file.
207 architecture: ElfArchitectureFormat,
208 },
209
210 /// Explicit representation of an ELF file's soname data.
211 ///
212 /// Tracks the `name` of a _shared object_ file, the `version` of its _soname_ and its
213 /// `architecture`. This form is used if the _soname data_ of a _shared object_ exposes a
214 /// specific version.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use std::str::FromStr;
220 ///
221 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
222 ///
223 /// # fn main() -> Result<(), alpm_types::Error> {
224 /// let soname = SonameV1::from_str("example.so=1.0.0-64")?;
225 /// assert_eq!(
226 /// soname,
227 /// SonameV1::Explicit {
228 /// name: "example.so".parse()?,
229 /// version: "1.0.0".parse()?,
230 /// architecture: ElfArchitectureFormat::Bit64,
231 /// }
232 /// );
233 /// # Ok(())
234 /// # }
235 Explicit {
236 /// The least specific name of the shared object file.
237 name: SharedObjectName,
238 /// The version of the shared object file (as exposed in its _soname_ data).
239 version: PackageVersion,
240 /// The ELF architecture format of the shared object file.
241 architecture: ElfArchitectureFormat,
242 },
243}
244
245impl SonameV1 {
246 /// Creates a new [`SonameV1`].
247 ///
248 /// Depending on input, this function returns different variants of [`SonameV1`]:
249 ///
250 /// - [`SonameV1::Basic`], if both `version_or_soname` and `architecture` are [`None`]
251 /// - [`SonameV1::Unversioned`], if `version_or_soname` is [`VersionOrSoname::Soname`] and
252 /// `architecture` is [`Some`]
253 /// - [`SonameV1::Explicit`], if `version_or_soname` is [`VersionOrSoname::Version`] and
254 /// `architecture` is [`Some`]
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
260 ///
261 /// # fn main() -> Result<(), alpm_types::Error> {
262 /// let basic_soname = SonameV1::new("example.so".parse()?, None, None)?;
263 /// assert_eq!(basic_soname, SonameV1::Basic("example.so".parse()?));
264 ///
265 /// let unversioned_soname = SonameV1::new(
266 /// "example.so".parse()?,
267 /// Some("example.so".parse()?),
268 /// Some(ElfArchitectureFormat::Bit64),
269 /// )?;
270 /// assert_eq!(
271 /// unversioned_soname,
272 /// SonameV1::Unversioned {
273 /// name: "example.so".parse()?,
274 /// soname: "example.so".parse()?,
275 /// architecture: "64".parse()?
276 /// }
277 /// );
278 ///
279 /// let explicit_soname = SonameV1::new(
280 /// "example.so".parse()?,
281 /// Some("1.0.0".parse()?),
282 /// Some(ElfArchitectureFormat::Bit64),
283 /// )?;
284 /// assert_eq!(
285 /// explicit_soname,
286 /// SonameV1::Explicit {
287 /// name: "example.so".parse()?,
288 /// version: "1.0.0".parse()?,
289 /// architecture: "64".parse()?
290 /// }
291 /// );
292 /// # Ok(())
293 /// # }
294 /// ```
295 pub fn new(
296 name: SharedObjectName,
297 version_or_soname: Option<VersionOrSoname>,
298 architecture: Option<ElfArchitectureFormat>,
299 ) -> Result<Self, Error> {
300 match (version_or_soname, architecture) {
301 (None, None) => Ok(Self::Basic(name)),
302 (Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
303 name,
304 version,
305 architecture,
306 }),
307 (Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
308 name,
309 soname,
310 architecture,
311 }),
312 (None, Some(_)) => Err(Error::InvalidSonameV1(
313 "SonameV1 needs a version when specifying architecture",
314 )),
315 (Some(_), None) => Err(Error::InvalidSonameV1(
316 "SonameV1 needs an architecture when specifying version",
317 )),
318 }
319 }
320
321 /// Parses a [`SonameV1`] from a string slice.
322 pub fn parser(input: &mut &str) -> ModalResult<Self> {
323 // Parse the shared object name.
324 let name = Self::parse_shared_object_name(input)?;
325
326 // Parse the version delimiter `=`.
327 //
328 // If it doesn't exist, it is the basic form.
329 if Self::parse_version_delimiter(input).is_err() {
330 return Ok(SonameV1::Basic(name));
331 }
332
333 // Take all input until we hit the delimiter and architecture.
334 let (raw_version_or_soname, _): (String, _) =
335 cut_err(repeat_till(1.., any, peek(("-", digit1, eof))))
336 .context(StrContext::Expected(StrContextValue::Description(
337 "a version or shared object name, followed by an ELF architecture format",
338 )))
339 .parse_next(input)?;
340
341 // Two cases are possible here:
342 //
343 // 1. Unversioned: `name=soname-architecture`
344 // 2. Explicit: `name=version-architecture`
345 let version_or_soname =
346 VersionOrSoname::parser.parse_next(&mut raw_version_or_soname.as_str())?;
347
348 // Parse the `-` delimiter
349 Self::parse_architecture_delimiter(input)?;
350
351 // Parse the architecture
352 let architecture = Self::parse_architecture(input)?;
353
354 match version_or_soname {
355 VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
356 name,
357 version,
358 architecture,
359 }),
360 VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
361 name,
362 soname,
363 architecture,
364 }),
365 }
366 }
367
368 /// Parses the shared object name until the version delimiter `=`.
369 fn parse_shared_object_name(input: &mut &str) -> ModalResult<SharedObjectName> {
370 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
376 /// Parses the version delimiter `=`.
377 ///
378 /// This function discards the result for only checking if the version delimiter is present.
379 fn parse_version_delimiter(input: &mut &str) -> ModalResult<()> {
380 cut_err("=")
381 .context(StrContext::Label("version delimiter"))
382 .context(StrContext::Expected(StrContextValue::Description(
383 "version delimiter `=`",
384 )))
385 .parse_next(input)
386 .map(|_| ())
387 }
388
389 /// Parses the architecture delimiter `-`.
390 fn parse_architecture_delimiter(input: &mut &str) -> ModalResult<()> {
391 cut_err("-")
392 .context(StrContext::Label("architecture delimiter"))
393 .context(StrContext::Expected(StrContextValue::Description(
394 "architecture delimiter `-`",
395 )))
396 .parse_next(input)
397 .map(|_| ())
398 }
399
400 /// Parses the architecture.
401 fn parse_architecture(input: &mut &str) -> ModalResult<ElfArchitectureFormat> {
402 cut_err(take_while(1.., |c: char| c.is_ascii_digit()))
403 .try_map(ElfArchitectureFormat::from_str)
404 .context(StrContext::Label("architecture"))
405 .parse_next(input)
406 }
407}
408
409impl FromStr for SonameV1 {
410 type Err = Error;
411 /// Parses a [`SonameV1`] from a string slice.
412 ///
413 /// The string slice must be in the format `name[=version-architecture]`.
414 ///
415 /// # Errors
416 ///
417 /// Returns an error if a [`SonameV1`] can not be parsed from input.
418 ///
419 /// # Examples
420 ///
421 /// ```
422 /// use std::str::FromStr;
423 ///
424 /// use alpm_types::{ElfArchitectureFormat, SonameV1};
425 ///
426 /// # fn main() -> Result<(), alpm_types::Error> {
427 /// assert_eq!(
428 /// SonameV1::from_str("example.so=1.0.0-64")?,
429 /// SonameV1::Explicit {
430 /// name: "example.so".parse()?,
431 /// version: "1.0.0".parse()?,
432 /// architecture: ElfArchitectureFormat::Bit64,
433 /// },
434 /// );
435 /// assert_eq!(
436 /// SonameV1::from_str("example.so=example.so-64")?,
437 /// SonameV1::Unversioned {
438 /// name: "example.so".parse()?,
439 /// soname: "example.so".parse()?,
440 /// architecture: ElfArchitectureFormat::Bit64,
441 /// },
442 /// );
443 /// assert_eq!(
444 /// SonameV1::from_str("example.so")?,
445 /// SonameV1::Basic("example.so".parse()?),
446 /// );
447 /// # Ok(())
448 /// # }
449 /// ```
450 fn from_str(s: &str) -> Result<Self, Self::Err> {
451 Ok(Self::parser.parse(s)?)
452 }
453}
454
455impl Display for SonameV1 {
456 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
457 match self {
458 Self::Basic(name) => write!(f, "{name}"),
459 Self::Unversioned {
460 name,
461 soname,
462 architecture,
463 } => write!(f, "{name}={soname}-{architecture}"),
464 Self::Explicit {
465 name,
466 version,
467 architecture,
468 } => write!(f, "{name}={version}-{architecture}"),
469 }
470 }
471}
472
473/// A prefix associated with a library lookup directory.
474///
475/// Library lookup directories are used when detecting shared object files on a system.
476/// Each such lookup directory can be assigned to a _prefix_, which allows identifying them in other
477/// contexts. E.g. `lib` may serve as _prefix_ for the lookup directory `/usr/lib`.
478///
479/// This is a type alias for [`Name`].
480pub type SharedLibraryPrefix = Name;
481
482/// The value of a shared object's _soname_.
483///
484/// This data may be present in the _SONAME_ or _NEEDED_ fields of a shared object's _dynamic
485/// section_.
486///
487/// The _soname_ data may contain only a shared object name (e.g. `libexample.so`) or a shared
488/// object name, that also encodes version information (e.g. `libexample.so.1`).
489#[derive(Clone, Debug, Eq, PartialEq)]
490pub struct Soname {
491 /// The name part of a shared object's _soname_.
492 pub name: SharedObjectName,
493 /// The optional version part of a shared object's _soname_.
494 pub version: Option<PackageVersion>,
495}
496
497impl Soname {
498 /// Creates a new [`Soname`].
499 pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
500 Self { name, version }
501 }
502
503 /// Recognizes a [`Soname`] in a string slice.
504 ///
505 /// The passed data can be in the following formats:
506 ///
507 /// - `<name>.so`: A shared object name without a version. (e.g. `libexample.so`)
508 /// - `<name>.so.<version>`: A shared object name with a version. (e.g. `libexample.so.1`)
509 /// - The version must be a valid [`PackageVersion`].
510 pub fn parser(input: &mut &str) -> ModalResult<Self> {
511 let name = cut_err(
512 (
513 // Parse the name of the shared object until eof or the `.so` is hit.
514 repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof)))),
515 // Parse at least one or more `.so` suffix(es).
516 cut_err(repeat::<_, _, String, _, _>(1.., ".so"))
517 .context(StrContext::Label("suffix"))
518 .context(StrContext::Expected(StrContextValue::Description(
519 "shared object name suffix '.so'",
520 ))),
521 )
522 // Take both parts and map them onto a SharedObjectName
523 .take()
524 .and_then(Name::parser)
525 .map(SharedObjectName),
526 )
527 .context(StrContext::Label("shared object name"))
528 .parse_next(input)?;
529
530 // Parse the version delimiter.
531 let delimiter = cut_err(alt((".", eof)))
532 .context(StrContext::Label("version delimiter"))
533 .context(StrContext::Expected(StrContextValue::Description(
534 "version delimiter `.`",
535 )))
536 .parse_next(input)?;
537
538 // If a `.` is found, map the rest of the string to a version.
539 // Otherwise, we hit the `eof` and there's no version.
540 let version = match delimiter {
541 "" => None,
542 "." => Some(rest.and_then(PackageVersion::parser).parse_next(input)?),
543 _ => unreachable!(),
544 };
545
546 Ok(Self { name, version })
547 }
548}
549
550impl Display for Soname {
551 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
552 match &self.version {
553 Some(version) => write!(f, "{name}.{version}", name = self.name),
554 None => write!(f, "{name}", name = self.name),
555 }
556 }
557}
558
559impl FromStr for Soname {
560 type Err = Error;
561
562 /// Recognizes a [`Soname`] in a string slice.
563 ///
564 /// The string slice must be in the format of `<name>.so` or `<name>.so.<version>`.
565 ///
566 /// # Errors
567 ///
568 /// Returns an error if a [`Soname`] can not be parsed from input.
569 ///
570 /// # Examples
571 ///
572 /// ```
573 /// use std::str::FromStr;
574 ///
575 /// use alpm_types::Soname;
576 /// # fn main() -> Result<(), alpm_types::Error> {
577 /// assert_eq!(
578 /// Soname::from_str("libexample.so.1")?,
579 /// Soname::new("libexample.so".parse()?, Some("1".parse()?)),
580 /// );
581 /// assert_eq!(
582 /// Soname::from_str("libexample.so")?,
583 /// Soname::new("libexample.so".parse()?, None),
584 /// );
585 /// # Ok(())
586 /// # }
587 /// ```
588 fn from_str(s: &str) -> Result<Self, Self::Err> {
589 Ok(Self::parser.parse(s)?)
590 }
591}
592
593/// Representation of [soname] data of a shared object based on the [alpm-sonamev2] specification.
594///
595/// Soname data may be used as [alpm-package-relation] of type _provision_ or _run-time dependency_
596/// in [`PackageInfoV1`] and [`PackageInfoV2`]. The data consists of the arbitrarily
597/// defined `prefix`, which denotes the use name of a specific library directory, and the `soname`,
598/// which refers to the value of either the _SONAME_ or a _NEEDED_ field in the _dynamic section_ of
599/// an [ELF] file.
600///
601/// # Examples
602///
603/// This example assumpes that `lib` is used as the `prefix` for the library directory `/usr/lib`
604/// and the following files are contained in it:
605///
606/// ```bash
607/// /usr/lib/libexample.so -> libexample.so.1
608/// /usr/lib/libexample.so.1 -> libexample.so.1.0.0
609/// /usr/lib/libexample.so.1.0.0
610/// ```
611///
612/// The above file `/usr/lib/libexample.so.1.0.0` represents an [ELF] file, that exposes
613/// `libexample.so.1` as value of the _SONAME_ field in its _dynamic section_. This data can be
614/// represented as follows, using [`SonameV2`]:
615///
616/// ```rust
617/// use alpm_types::{Soname, SonameV2};
618///
619/// # fn main() -> Result<(), alpm_types::Error> {
620/// let soname_data = SonameV2 {
621/// prefix: "lib".parse()?,
622/// soname: Soname {
623/// name: "libexample.so".parse()?,
624/// version: Some("1".parse()?),
625/// },
626/// };
627/// assert_eq!(soname_data.to_string(), "lib:libexample.so.1");
628/// # Ok(())
629/// # }
630/// ```
631///
632/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
633/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
634/// [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
635/// [soname]: https://en.wikipedia.org/wiki/Soname
636/// [`PackageInfoV1`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV1.html
637/// [`PackageInfoV2`]: https://docs.rs/alpm_pkginfo/latest/alpm_pkginfo/struct.PackageInfoV2.html
638#[derive(Clone, Debug, Eq, PartialEq)]
639pub struct SonameV2 {
640 /// The directory prefix of the shared object file.
641 pub prefix: SharedLibraryPrefix,
642 /// The _soname_ of a shared object file.
643 pub soname: Soname,
644}
645
646impl SonameV2 {
647 /// Creates a new [`SonameV2`].
648 ///
649 /// # Examples
650 ///
651 /// ```
652 /// use alpm_types::SonameV2;
653 ///
654 /// # fn main() -> Result<(), alpm_types::Error> {
655 /// SonameV2::new("lib".parse()?, "libexample.so.1".parse()?);
656 /// # Ok(())
657 /// # }
658 /// ```
659 pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
660 Self { prefix, soname }
661 }
662
663 /// Recognizes a [`SonameV2`] in a string slice.
664 ///
665 /// The passed data must be in the format `<prefix>:<soname>`. (e.g. `lib:libexample.so.1`)
666 ///
667 /// See [`Soname::parser`] for details on the format of `<soname>`.
668 ///
669 /// # Errors
670 ///
671 /// Returns an error if no [`SonameV2`] can be created from `input`.
672 pub fn parser(input: &mut &str) -> ModalResult<Self> {
673 // Parse everything from the start to the first `:` and parse as `SharedLibraryPrefix`.
674 let prefix = cut_err(
675 repeat_till(1.., any, peek(alt((":", eof))))
676 .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
677 )
678 .context(StrContext::Label("prefix for a shared object lookup path"))
679 .parse_next(input)?;
680
681 cut_err(":")
682 .context(StrContext::Label("shared library prefix delimiter"))
683 .context(StrContext::Expected(StrContextValue::Description(
684 "shared library prefix `:`",
685 )))
686 .parse_next(input)?;
687
688 let soname = Soname::parser.parse_next(input)?;
689
690 Ok(Self { prefix, soname })
691 }
692}
693
694impl FromStr for SonameV2 {
695 type Err = Error;
696
697 /// Parses a [`SonameV2`] from a string slice.
698 ///
699 /// The string slice must be in the format `<prefix>:<soname>`.
700 ///
701 /// # Errors
702 ///
703 /// Returns an error if a [`SonameV2`] can not be parsed from input.
704 ///
705 /// # Examples
706 ///
707 /// ```
708 /// use std::str::FromStr;
709 ///
710 /// use alpm_types::{Soname, SonameV2};
711 ///
712 /// # fn main() -> Result<(), alpm_types::Error> {
713 /// assert_eq!(
714 /// SonameV2::from_str("lib:libexample.so.1")?,
715 /// SonameV2::new(
716 /// "lib".parse()?,
717 /// Soname::new("libexample.so".parse()?, Some("1".parse()?))
718 /// ),
719 /// );
720 /// # Ok(())
721 /// # }
722 /// ```
723 fn from_str(s: &str) -> Result<Self, Self::Err> {
724 Ok(Self::parser.parse(s)?)
725 }
726}
727
728impl Display for SonameV2 {
729 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
730 write!(
731 f,
732 "{prefix}:{soname}",
733 prefix = self.prefix,
734 soname = self.soname
735 )
736 }
737}
738
739/// A package relation
740///
741/// Describes a relation to a component.
742/// Package relations may either consist of only a [`Name`] *or* of a [`Name`] and a
743/// [`VersionRequirement`].
744///
745/// ## Note
746///
747/// A [`PackageRelation`] covers all [alpm-package-relations] *except* optional
748/// dependencies, as those behave differently.
749///
750/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
751#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
752pub struct PackageRelation {
753 /// The name of the package
754 pub name: Name,
755 /// The version requirement for the package
756 pub version_requirement: Option<VersionRequirement>,
757}
758
759impl PackageRelation {
760 /// Creates a new [`PackageRelation`]
761 ///
762 /// # Examples
763 ///
764 /// ```
765 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
766 ///
767 /// # fn main() -> Result<(), alpm_types::Error> {
768 /// PackageRelation::new(
769 /// "example".parse()?,
770 /// Some(VersionRequirement {
771 /// comparison: VersionComparison::Less,
772 /// version: "1.0.0".parse()?,
773 /// }),
774 /// );
775 ///
776 /// PackageRelation::new("example".parse()?, None);
777 /// # Ok(())
778 /// # }
779 /// ```
780 pub fn new(name: Name, version_requirement: Option<VersionRequirement>) -> Self {
781 Self {
782 name,
783 version_requirement,
784 }
785 }
786
787 /// Parses a [`PackageRelation`] from a string slice.
788 ///
789 /// Consumes all of its input.
790 ///
791 /// # Examples
792 ///
793 /// See [`Self::from_str`] for code examples.
794 ///
795 /// # Errors
796 ///
797 /// Returns an error if `input` is not a valid _package-relation_.
798 pub fn parser(input: &mut &str) -> ModalResult<Self> {
799 seq!(Self {
800 name: take_till(1.., ('<', '>', '=')).and_then(Name::parser).context(StrContext::Label("package name")),
801 version_requirement: opt(VersionRequirement::parser),
802 _: eof.context(StrContext::Expected(StrContextValue::Description("end of relation version requirement"))),
803 })
804 .parse_next(input)
805 }
806}
807
808impl Display for PackageRelation {
809 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
810 if let Some(version_requirement) = self.version_requirement.as_ref() {
811 write!(f, "{}{}", self.name, version_requirement)
812 } else {
813 write!(f, "{}", self.name)
814 }
815 }
816}
817
818impl FromStr for PackageRelation {
819 type Err = Error;
820 /// Parses a [`PackageRelation`] from a string slice.
821 ///
822 /// Delegates to [`PackageRelation::parser`].
823 ///
824 /// # Errors
825 ///
826 /// Returns an error if [`PackageRelation::parser`] fails.
827 ///
828 /// # Examples
829 ///
830 /// ```
831 /// use std::str::FromStr;
832 ///
833 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
834 ///
835 /// # fn main() -> Result<(), alpm_types::Error> {
836 /// assert_eq!(
837 /// PackageRelation::from_str("example<1.0.0")?,
838 /// PackageRelation::new(
839 /// "example".parse()?,
840 /// Some(VersionRequirement {
841 /// comparison: VersionComparison::Less,
842 /// version: "1.0.0".parse()?
843 /// })
844 /// ),
845 /// );
846 ///
847 /// assert_eq!(
848 /// PackageRelation::from_str("example<=1.0.0")?,
849 /// PackageRelation::new(
850 /// "example".parse()?,
851 /// Some(VersionRequirement {
852 /// comparison: VersionComparison::LessOrEqual,
853 /// version: "1.0.0".parse()?
854 /// })
855 /// ),
856 /// );
857 ///
858 /// assert_eq!(
859 /// PackageRelation::from_str("example=1.0.0")?,
860 /// PackageRelation::new(
861 /// "example".parse()?,
862 /// Some(VersionRequirement {
863 /// comparison: VersionComparison::Equal,
864 /// version: "1.0.0".parse()?
865 /// })
866 /// ),
867 /// );
868 ///
869 /// assert_eq!(
870 /// PackageRelation::from_str("example>1.0.0")?,
871 /// PackageRelation::new(
872 /// "example".parse()?,
873 /// Some(VersionRequirement {
874 /// comparison: VersionComparison::Greater,
875 /// version: "1.0.0".parse()?
876 /// })
877 /// ),
878 /// );
879 ///
880 /// assert_eq!(
881 /// PackageRelation::from_str("example>=1.0.0")?,
882 /// PackageRelation::new(
883 /// "example".parse()?,
884 /// Some(VersionRequirement {
885 /// comparison: VersionComparison::GreaterOrEqual,
886 /// version: "1.0.0".parse()?
887 /// })
888 /// ),
889 /// );
890 ///
891 /// assert_eq!(
892 /// PackageRelation::from_str("example")?,
893 /// PackageRelation::new("example".parse()?, None),
894 /// );
895 ///
896 /// assert!(PackageRelation::from_str("example<").is_err());
897 /// # Ok(())
898 /// # }
899 /// ```
900 fn from_str(s: &str) -> Result<Self, Self::Err> {
901 Ok(Self::parser.parse(s)?)
902 }
903}
904
905/// An optional dependency for a package.
906///
907/// This type is used for representing dependencies that are not essential for base functionality
908/// of a package, but may be necessary to make use of certain features of a package.
909///
910/// An [`OptionalDependency`] consists of a package relation and an optional description separated
911/// by a colon (`:`).
912///
913/// - The package relation component must be a valid [`PackageRelation`].
914/// - If a description is provided it must be at least one character long.
915///
916/// Refer to [alpm-package-relation] of type [optional dependency] for details on the format.
917/// ## Examples
918///
919/// ```
920/// use std::str::FromStr;
921///
922/// use alpm_types::{Name, OptionalDependency};
923///
924/// # fn main() -> Result<(), alpm_types::Error> {
925/// // Create OptionalDependency from &str
926/// let opt_depend = OptionalDependency::from_str("example: this is an example dependency")?;
927///
928/// // Get the name
929/// assert_eq!("example", opt_depend.name().as_ref());
930///
931/// // Get the description
932/// assert_eq!(
933/// Some("this is an example dependency"),
934/// opt_depend.description().as_deref()
935/// );
936///
937/// // Format as String
938/// assert_eq!(
939/// "example: this is an example dependency",
940/// format!("{opt_depend}")
941/// );
942/// # Ok(())
943/// # }
944/// ```
945///
946/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
947/// [optional dependency]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html#optional-dependency
948#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
949pub struct OptionalDependency {
950 package_relation: PackageRelation,
951 description: Option<String>,
952}
953
954impl OptionalDependency {
955 /// Create a new OptionalDependency in a Result
956 pub fn new(
957 package_relation: PackageRelation,
958 description: Option<String>,
959 ) -> OptionalDependency {
960 OptionalDependency {
961 package_relation,
962 description,
963 }
964 }
965
966 /// Return the name of the optional dependency
967 pub fn name(&self) -> &Name {
968 &self.package_relation.name
969 }
970
971 /// Return the version requirement of the optional dependency
972 pub fn version_requirement(&self) -> &Option<VersionRequirement> {
973 &self.package_relation.version_requirement
974 }
975
976 /// Return the description for the optional dependency, if it exists
977 pub fn description(&self) -> &Option<String> {
978 &self.description
979 }
980
981 /// Recognizes an [`OptionalDependency`] in a string slice.
982 ///
983 /// Consumes all of its input.
984 ///
985 /// # Errors
986 ///
987 /// Returns an error if `input` is not a valid _alpm-package-relation_ of type _optional
988 /// dependency_.
989 pub fn parser(input: &mut &str) -> ModalResult<Self> {
990 let description_parser = terminated(
991 // Descriptions may consist of any character except '\n' and '\r'.
992 // Descriptions are a also at the end of a `OptionalDependency`.
993 // We enforce forbidding `\n` and `\r` by only taking until either of them
994 // is hit and checking for `eof` afterwards.
995 // This will **always** succeed unless `\n` and `\r` are hit, in which case an
996 // error is thrown.
997 take_till(0.., ('\n', '\r')),
998 eof,
999 )
1000 .context(StrContext::Label("optional dependency description"))
1001 .context(StrContext::Expected(StrContextValue::Description(
1002 r"no carriage returns or newlines",
1003 )))
1004 .map(|d: &str| match d.trim_ascii() {
1005 "" => None,
1006 t => Some(t.to_string()),
1007 });
1008
1009 let (package_relation, description) = alt((
1010 // look for ": ", then dispatch either side to the relevant parser
1011 // without allowing backtracking
1012 separated_pair(
1013 take_until(1.., ": ").and_then(cut_err(PackageRelation::parser)),
1014 ": ",
1015 rest.and_then(cut_err(description_parser)),
1016 ),
1017 // if we can't find ": ", then assume it's all PackageRelation
1018 // and assert we've reached the end of input
1019 (rest.and_then(PackageRelation::parser), eof.value(None)),
1020 ))
1021 .parse_next(input)?;
1022
1023 Ok(Self {
1024 package_relation,
1025 description,
1026 })
1027 }
1028}
1029
1030impl FromStr for OptionalDependency {
1031 type Err = Error;
1032
1033 /// Creates a new [`OptionalDependency`] from a string slice.
1034 ///
1035 /// Delegates to [`OptionalDependency::parser`].
1036 ///
1037 /// # Errors
1038 ///
1039 /// Returns an error if [`OptionalDependency::parser`] fails.
1040 fn from_str(s: &str) -> Result<Self, Self::Err> {
1041 Ok(Self::parser.parse(s)?)
1042 }
1043}
1044
1045impl Display for OptionalDependency {
1046 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
1047 match self.description {
1048 Some(ref description) => write!(fmt, "{}: {}", self.name(), description),
1049 None => write!(fmt, "{}", self.name()),
1050 }
1051 }
1052}
1053
1054/// Group of a package
1055///
1056/// Represents an arbitrary collection of packages that share a common
1057/// characteristic or functionality.
1058///
1059/// While group names can be any valid UTF-8 string, it is recommended to follow
1060/// the format of [`Name`] (`[a-z\d\-._@+]` but must not start with `[-.]`)
1061/// to ensure consistency and ease of use.
1062///
1063/// This is a type alias for [`String`].
1064///
1065/// ## Examples
1066/// ```
1067/// use alpm_types::Group;
1068///
1069/// // Create a Group
1070/// let group: Group = "package-group".to_string();
1071/// ```
1072pub type Group = String;
1073
1074#[cfg(test)]
1075mod tests {
1076 use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
1077 use rstest::rstest;
1078
1079 use super::*;
1080 use crate::VersionComparison;
1081
1082 const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
1083 /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
1084 /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
1085 /// we don't test that fully
1086 const EPOCH_REGEX: &str = r"[1-9]{1}[0-9]{0,10}";
1087 const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
1088 const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
1089 const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
1090 const DESCRIPTION_REGEX: &str = "[^\n\r]*";
1091
1092 proptest! {
1093 #![proptest_config(ProptestConfig::with_cases(1000))]
1094
1095
1096 #[test]
1097 fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
1098 println!("s: {s}");
1099 let name = PackageRelation::from_str(&s).unwrap();
1100 prop_assert_eq!(s, format!("{}", name));
1101 }
1102 }
1103
1104 proptest! {
1105 #[test]
1106 fn opt_depend_from_str(
1107 name in NAME_REGEX,
1108 desc in DESCRIPTION_REGEX,
1109 use_desc in proptest::bool::ANY
1110 ) {
1111 let desc_trimmed = desc.trim_ascii();
1112 let desc_is_blank = desc_trimmed.is_empty();
1113
1114 let (raw_in, formatted_expected) = if use_desc {
1115 // Raw input and expected formatted output.
1116 // These are different because `desc` will be trimmed by the parser;
1117 // if it is *only* ascii whitespace then it will be skipped altogether.
1118 (
1119 format!("{name}: {desc}"),
1120 if !desc_is_blank {
1121 format!("{name}: {desc_trimmed}")
1122 } else {
1123 name.clone()
1124 }
1125 )
1126 } else {
1127 (name.clone(), name.clone())
1128 };
1129
1130 println!("input string: {raw_in}");
1131 let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
1132 let formatted_actual = format!("{}", opt_depend);
1133 prop_assert_eq!(
1134 formatted_expected,
1135 formatted_actual,
1136 "Formatted output doesn't match input"
1137 );
1138 }
1139 }
1140
1141 #[rstest]
1142 #[case(
1143 "python>=3",
1144 Ok(PackageRelation {
1145 name: Name::new("python").unwrap(),
1146 version_requirement: Some(VersionRequirement {
1147 comparison: VersionComparison::GreaterOrEqual,
1148 version: "3".parse().unwrap(),
1149 }),
1150 }),
1151 )]
1152 #[case(
1153 "java-environment>=17",
1154 Ok(PackageRelation {
1155 name: Name::new("java-environment").unwrap(),
1156 version_requirement: Some(VersionRequirement {
1157 comparison: VersionComparison::GreaterOrEqual,
1158 version: "17".parse().unwrap(),
1159 }),
1160 }),
1161 )]
1162 fn valid_package_relation(
1163 #[case] input: &str,
1164 #[case] expected: Result<PackageRelation, Error>,
1165 ) {
1166 assert_eq!(PackageRelation::from_str(input), expected);
1167 }
1168
1169 #[rstest]
1170 #[case(
1171 "example: this is an example dependency",
1172 Ok(OptionalDependency {
1173 package_relation: PackageRelation {
1174 name: Name::new("example").unwrap(),
1175 version_requirement: None,
1176 },
1177 description: Some("this is an example dependency".to_string()),
1178 }),
1179 )]
1180 #[case(
1181 "example-two: a description with lots of whitespace padding ",
1182 Ok(OptionalDependency {
1183 package_relation: PackageRelation {
1184 name: Name::new("example-two").unwrap(),
1185 version_requirement: None,
1186 },
1187 description: Some("a description with lots of whitespace padding".to_string())
1188 }),
1189 )]
1190 #[case(
1191 "dep_name",
1192 Ok(OptionalDependency {
1193 package_relation: PackageRelation {
1194 name: Name::new("dep_name").unwrap(),
1195 version_requirement: None,
1196 },
1197 description: None,
1198 }),
1199 )]
1200 #[case(
1201 "dep_name: ",
1202 Ok(OptionalDependency {
1203 package_relation: PackageRelation {
1204 name: Name::new("dep_name").unwrap(),
1205 version_requirement: None,
1206 },
1207 description: None,
1208 }),
1209 )]
1210 #[case(
1211 "dep_name_with_special_chars-123: description with !@#$%^&*",
1212 Ok(OptionalDependency {
1213 package_relation: PackageRelation {
1214 name: Name::new("dep_name_with_special_chars-123").unwrap(),
1215 version_requirement: None,
1216 },
1217 description: Some("description with !@#$%^&*".to_string()),
1218 }),
1219 )]
1220 // versioned optional dependencies
1221 #[case(
1222 "elfutils=0.192: for translations",
1223 Ok(OptionalDependency {
1224 package_relation: PackageRelation {
1225 name: Name::new("elfutils").unwrap(),
1226 version_requirement: Some(VersionRequirement {
1227 comparison: VersionComparison::Equal,
1228 version: "0.192".parse().unwrap(),
1229 }),
1230 },
1231 description: Some("for translations".to_string()),
1232 }),
1233 )]
1234 #[case(
1235 "python>=3: For Python bindings",
1236 Ok(OptionalDependency {
1237 package_relation: PackageRelation {
1238 name: Name::new("python").unwrap(),
1239 version_requirement: Some(VersionRequirement {
1240 comparison: VersionComparison::GreaterOrEqual,
1241 version: "3".parse().unwrap(),
1242 }),
1243 },
1244 description: Some("For Python bindings".to_string()),
1245 }),
1246 )]
1247 #[case(
1248 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1249 Ok(OptionalDependency {
1250 package_relation: PackageRelation {
1251 name: Name::new("java-environment").unwrap(),
1252 version_requirement: Some(VersionRequirement {
1253 comparison: VersionComparison::GreaterOrEqual,
1254 version: "17".parse().unwrap(),
1255 }),
1256 },
1257 description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
1258 }),
1259 )]
1260 fn opt_depend_from_string(
1261 #[case] input: &str,
1262 #[case] expected_result: Result<OptionalDependency, Error>,
1263 ) {
1264 let opt_depend_result = OptionalDependency::from_str(input);
1265 assert_eq!(expected_result, opt_depend_result);
1266 }
1267
1268 #[rstest]
1269 #[case(
1270 "#invalid-name: this is an example dependency",
1271 "invalid first character of package name"
1272 )]
1273 #[case(": no_name_colon", "invalid first character of package name")]
1274 #[case(
1275 "name:description with no leading whitespace",
1276 "invalid character in package name"
1277 )]
1278 #[case(
1279 "dep-name>=10: \n\ndescription with\rnewlines",
1280 "expected no carriage returns or newlines"
1281 )]
1282 fn opt_depend_invalid_string_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
1283 let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
1284 panic!("'{input}' did not fail to parse as expected")
1285 };
1286 assert!(
1287 err_msg.contains(err_snippet),
1288 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1289 );
1290 }
1291
1292 #[rstest]
1293 #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
1294 #[case("example.so=1.0.0-64", SonameV1::Explicit {
1295 name: "example.so".parse().unwrap(),
1296 version: "1.0.0".parse().unwrap(),
1297 architecture: ElfArchitectureFormat::Bit64,
1298 })]
1299 fn sonamev1_from_string(
1300 #[case] input: &str,
1301 #[case] expected_result: SonameV1,
1302 ) -> testresult::TestResult<()> {
1303 let soname = SonameV1::from_str(input)?;
1304 assert_eq!(expected_result, soname);
1305 assert_eq!(input, soname.to_string());
1306 Ok(())
1307 }
1308
1309 #[rstest]
1310 #[case(
1311 "libwlroots-0.18.so=libwlroots-0.18.so-64",
1312 SonameV1::Unversioned {
1313 name: "libwlroots-0.18.so".parse().unwrap(),
1314 soname: "libwlroots-0.18.so".parse().unwrap(),
1315 architecture: ElfArchitectureFormat::Bit64,
1316 },
1317 )]
1318 #[case(
1319 "libexample.so=otherlibexample.so-64",
1320 SonameV1::Unversioned {
1321 name: "libexample.so".parse().unwrap(),
1322 soname: "otherlibexample.so".parse().unwrap(),
1323 architecture: ElfArchitectureFormat::Bit64,
1324 },
1325 )]
1326 fn sonamev1_from_string_without_version(
1327 #[case] input: &str,
1328 #[case] expected_result: SonameV1,
1329 ) -> testresult::TestResult<()> {
1330 let soname = SonameV1::from_str(input)?;
1331 assert_eq!(expected_result, soname);
1332 assert_eq!(input, soname.to_string());
1333 Ok(())
1334 }
1335
1336 #[rstest]
1337 #[case("noso", "invalid shared object name")]
1338 #[case("invalidversion.so=1*2-64", "expected version or shared object name")]
1339 #[case(
1340 "nodelimiter.so=1.64",
1341 "expected a version or shared object name, followed by an ELF architecture format"
1342 )]
1343 #[case(
1344 "noarchitecture.so=1-",
1345 "expected a version or shared object name, followed by an ELF architecture format"
1346 )]
1347 #[case("invalidarchitecture.so=1-82", "invalid architecture")]
1348 #[case("invalidsoname.so~1.64", "unexpected trailing content")]
1349 fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
1350 let result = SonameV1::from_str(input);
1351 assert!(result.is_err(), "Expected SonameV1 parsing to fail");
1352 let err = result.unwrap_err();
1353 let pretty_error = err.to_string();
1354 assert!(
1355 pretty_error.contains(error_snippet),
1356 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1357 );
1358 }
1359
1360 #[rstest]
1361 #[case(
1362 "otherlibexample.so",
1363 VersionOrSoname::Soname(
1364 SharedObjectName::new("otherlibexample.so").unwrap())
1365 )]
1366 #[case(
1367 "1.0.0",
1368 VersionOrSoname::Version(
1369 PackageVersion::from_str("1.0.0").unwrap())
1370 )]
1371 fn version_or_soname_from_string(
1372 #[case] input: &str,
1373 #[case] expected_result: VersionOrSoname,
1374 ) -> testresult::TestResult<()> {
1375 let version = VersionOrSoname::from_str(input)?;
1376 assert_eq!(expected_result, version);
1377 assert_eq!(input, version.to_string());
1378 Ok(())
1379 }
1380
1381 #[rstest]
1382 #[case(
1383 "lib:libexample.so",
1384 SonameV2 {
1385 prefix: "lib".parse().unwrap(),
1386 soname: Soname {
1387 name: "libexample.so".parse().unwrap(),
1388 version: None,
1389 },
1390 },
1391 )]
1392 #[case(
1393 "usr:libexample.so.1",
1394 SonameV2 {
1395 prefix: "usr".parse().unwrap(),
1396 soname: Soname {
1397 name: "libexample.so".parse().unwrap(),
1398 version: "1".parse().ok(),
1399 },
1400 },
1401 )]
1402 #[case(
1403 "lib:libexample.so.1.2.3",
1404 SonameV2 {
1405 prefix: "lib".parse().unwrap(),
1406 soname: Soname {
1407 name: "libexample.so".parse().unwrap(),
1408 version: "1.2.3".parse().ok(),
1409 },
1410 },
1411 )]
1412 #[case(
1413 "lib:libexample.so.so.420",
1414 SonameV2 {
1415 prefix: "lib".parse().unwrap(),
1416 soname: Soname {
1417 name: "libexample.so.so".parse().unwrap(),
1418 version: "420".parse().ok(),
1419 },
1420 },
1421 )]
1422 #[case(
1423 "lib:libexample.so.test",
1424 SonameV2 {
1425 prefix: "lib".parse().unwrap(),
1426 soname: Soname {
1427 name: "libexample.so".parse().unwrap(),
1428 version: "test".parse().ok(),
1429 },
1430 },
1431 )]
1432 fn sonamev2_from_string(
1433 #[case] input: &str,
1434 #[case] expected_result: SonameV2,
1435 ) -> testresult::TestResult<()> {
1436 let soname = SonameV2::from_str(input)?;
1437 assert_eq!(expected_result, soname);
1438 assert_eq!(input, soname.to_string());
1439 Ok(())
1440 }
1441
1442 #[rstest]
1443 #[case("libexample.so.1", "invalid shared library prefix delimiter")]
1444 #[case("lib:libexample.so-abc", "invalid version delimiter")]
1445 #[case("lib:libexample.so.10-10", "invalid pkgver character")]
1446 #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
1447 fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
1448 let result = SonameV2::from_str(input);
1449 assert!(result.is_err(), "Expected SonameV2 parsing to fail");
1450 let err = result.unwrap_err();
1451 let pretty_error = err.to_string();
1452 assert!(
1453 pretty_error.contains(error_snippet),
1454 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1455 );
1456 }
1457}