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/// Provides either a [`PackageRelation`] or a [`SonameV1::Basic`].
1075///
1076/// This enum is used for [alpm-package-relations] of type _run-time dependency_ and _provision_ in
1077/// `.SRCINFO` files.
1078///
1079/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
1080#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1081#[serde(untagged)]
1082pub enum RelationOrSoname {
1083 /// A shared object name (as [`SonameV1::Basic`]).
1084 BasicSonameV1(SonameV1),
1085 /// A package relation (as [`PackageRelation`]).
1086 Relation(PackageRelation),
1087}
1088
1089impl PartialEq<PackageRelation> for RelationOrSoname {
1090 fn eq(&self, other: &PackageRelation) -> bool {
1091 self.to_string() == other.to_string()
1092 }
1093}
1094
1095impl PartialEq<SonameV1> for RelationOrSoname {
1096 fn eq(&self, other: &SonameV1) -> bool {
1097 self.to_string() == other.to_string()
1098 }
1099}
1100
1101impl RelationOrSoname {
1102 /// Recognizes a [`SonameV1::Basic`] or a [`PackageRelation`] in a string slice.
1103 ///
1104 /// First attempts to recognize a [`SonameV1::Basic`] and if that fails, falls back to
1105 /// recognizing a [`PackageRelation`].
1106 /// Depending on recognized type, a [`RelationOrSoname`] is created accordingly.
1107 pub fn parser(input: &mut &str) -> ModalResult<Self> {
1108 // Implement a custom `winnow::combinator::alt`, as all type parsers are built in
1109 // such a way that they return errors on unexpected input instead of backtracking.
1110 let checkpoint = input.checkpoint();
1111 let shared_object_name_result = SharedObjectName::parser.parse_next(input);
1112 if shared_object_name_result.is_ok() {
1113 let shared_object_name = shared_object_name_result?;
1114 return Ok(RelationOrSoname::BasicSonameV1(SonameV1::Basic(
1115 shared_object_name,
1116 )));
1117 }
1118
1119 input.reset(&checkpoint);
1120 let relation_result = rest.and_then(PackageRelation::parser).parse_next(input);
1121 if relation_result.is_ok() {
1122 let relation = relation_result?;
1123 return Ok(RelationOrSoname::Relation(relation));
1124 }
1125
1126 cut_err(fail)
1127 .context(StrContext::Expected(StrContextValue::Description(
1128 "alpm-sonamev1 or alpm-package-relation",
1129 )))
1130 .parse_next(input)
1131 }
1132}
1133
1134impl Display for RelationOrSoname {
1135 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1136 match self {
1137 RelationOrSoname::Relation(version) => write!(f, "{version}"),
1138 RelationOrSoname::BasicSonameV1(soname) => write!(f, "{soname}"),
1139 }
1140 }
1141}
1142
1143impl FromStr for RelationOrSoname {
1144 type Err = Error;
1145
1146 /// Creates a [`RelationOrSoname`] from a string slice.
1147 ///
1148 /// Relies on [`RelationOrSoname::parser`] to recognize types in `input` and create a
1149 /// [`RelationOrSoname`] accordingly.
1150 ///
1151 /// # Errors
1152 ///
1153 /// Returns an error if no [`RelationOrSoname`] can be created from `input`.
1154 ///
1155 /// # Examples
1156 ///
1157 /// ```
1158 /// use alpm_types::{PackageRelation, RelationOrSoname, SonameV1};
1159 ///
1160 /// # fn main() -> Result<(), alpm_types::Error> {
1161 /// let relation: RelationOrSoname = "example=1.0.0".parse()?;
1162 /// assert_eq!(
1163 /// relation,
1164 /// RelationOrSoname::Relation(PackageRelation::new(
1165 /// "example".parse()?,
1166 /// Some("=1.0.0".parse()?)
1167 /// ))
1168 /// );
1169 ///
1170 /// let soname: RelationOrSoname = "example.so".parse()?;
1171 /// assert_eq!(
1172 /// soname,
1173 /// RelationOrSoname::BasicSonameV1(SonameV1::new("example.so".parse()?, None, None)?)
1174 /// );
1175 /// # Ok(())
1176 /// # }
1177 /// ```
1178 fn from_str(s: &str) -> Result<Self, Self::Err> {
1179 Self::parser
1180 .parse(s)
1181 .map_err(|error| Error::ParseError(error.to_string()))
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
1188 use rstest::rstest;
1189 use testresult::TestResult;
1190
1191 use super::*;
1192 use crate::VersionComparison;
1193
1194 const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
1195 /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
1196 /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
1197 /// we don't test that fully
1198 const EPOCH_REGEX: &str = r"[1-9]{1}[0-9]{0,10}";
1199 const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
1200 const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
1201 const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
1202 const DESCRIPTION_REGEX: &str = "[^\n\r]*";
1203
1204 proptest! {
1205 #![proptest_config(ProptestConfig::with_cases(1000))]
1206
1207
1208 #[test]
1209 fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
1210 println!("s: {s}");
1211 let name = PackageRelation::from_str(&s).unwrap();
1212 prop_assert_eq!(s, format!("{}", name));
1213 }
1214 }
1215
1216 proptest! {
1217 #[test]
1218 fn opt_depend_from_str(
1219 name in NAME_REGEX,
1220 desc in DESCRIPTION_REGEX,
1221 use_desc in proptest::bool::ANY
1222 ) {
1223 let desc_trimmed = desc.trim_ascii();
1224 let desc_is_blank = desc_trimmed.is_empty();
1225
1226 let (raw_in, formatted_expected) = if use_desc {
1227 // Raw input and expected formatted output.
1228 // These are different because `desc` will be trimmed by the parser;
1229 // if it is *only* ascii whitespace then it will be skipped altogether.
1230 (
1231 format!("{name}: {desc}"),
1232 if !desc_is_blank {
1233 format!("{name}: {desc_trimmed}")
1234 } else {
1235 name.clone()
1236 }
1237 )
1238 } else {
1239 (name.clone(), name.clone())
1240 };
1241
1242 println!("input string: {raw_in}");
1243 let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
1244 let formatted_actual = format!("{}", opt_depend);
1245 prop_assert_eq!(
1246 formatted_expected,
1247 formatted_actual,
1248 "Formatted output doesn't match input"
1249 );
1250 }
1251 }
1252
1253 #[rstest]
1254 #[case(
1255 "python>=3",
1256 Ok(PackageRelation {
1257 name: Name::new("python").unwrap(),
1258 version_requirement: Some(VersionRequirement {
1259 comparison: VersionComparison::GreaterOrEqual,
1260 version: "3".parse().unwrap(),
1261 }),
1262 }),
1263 )]
1264 #[case(
1265 "java-environment>=17",
1266 Ok(PackageRelation {
1267 name: Name::new("java-environment").unwrap(),
1268 version_requirement: Some(VersionRequirement {
1269 comparison: VersionComparison::GreaterOrEqual,
1270 version: "17".parse().unwrap(),
1271 }),
1272 }),
1273 )]
1274 fn valid_package_relation(
1275 #[case] input: &str,
1276 #[case] expected: Result<PackageRelation, Error>,
1277 ) {
1278 assert_eq!(PackageRelation::from_str(input), expected);
1279 }
1280
1281 #[rstest]
1282 #[case(
1283 "example: this is an example dependency",
1284 Ok(OptionalDependency {
1285 package_relation: PackageRelation {
1286 name: Name::new("example").unwrap(),
1287 version_requirement: None,
1288 },
1289 description: Some("this is an example dependency".to_string()),
1290 }),
1291 )]
1292 #[case(
1293 "example-two: a description with lots of whitespace padding ",
1294 Ok(OptionalDependency {
1295 package_relation: PackageRelation {
1296 name: Name::new("example-two").unwrap(),
1297 version_requirement: None,
1298 },
1299 description: Some("a description with lots of whitespace padding".to_string())
1300 }),
1301 )]
1302 #[case(
1303 "dep_name",
1304 Ok(OptionalDependency {
1305 package_relation: PackageRelation {
1306 name: Name::new("dep_name").unwrap(),
1307 version_requirement: None,
1308 },
1309 description: None,
1310 }),
1311 )]
1312 #[case(
1313 "dep_name: ",
1314 Ok(OptionalDependency {
1315 package_relation: PackageRelation {
1316 name: Name::new("dep_name").unwrap(),
1317 version_requirement: None,
1318 },
1319 description: None,
1320 }),
1321 )]
1322 #[case(
1323 "dep_name_with_special_chars-123: description with !@#$%^&*",
1324 Ok(OptionalDependency {
1325 package_relation: PackageRelation {
1326 name: Name::new("dep_name_with_special_chars-123").unwrap(),
1327 version_requirement: None,
1328 },
1329 description: Some("description with !@#$%^&*".to_string()),
1330 }),
1331 )]
1332 // versioned optional dependencies
1333 #[case(
1334 "elfutils=0.192: for translations",
1335 Ok(OptionalDependency {
1336 package_relation: PackageRelation {
1337 name: Name::new("elfutils").unwrap(),
1338 version_requirement: Some(VersionRequirement {
1339 comparison: VersionComparison::Equal,
1340 version: "0.192".parse().unwrap(),
1341 }),
1342 },
1343 description: Some("for translations".to_string()),
1344 }),
1345 )]
1346 #[case(
1347 "python>=3: For Python bindings",
1348 Ok(OptionalDependency {
1349 package_relation: PackageRelation {
1350 name: Name::new("python").unwrap(),
1351 version_requirement: Some(VersionRequirement {
1352 comparison: VersionComparison::GreaterOrEqual,
1353 version: "3".parse().unwrap(),
1354 }),
1355 },
1356 description: Some("For Python bindings".to_string()),
1357 }),
1358 )]
1359 #[case(
1360 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
1361 Ok(OptionalDependency {
1362 package_relation: PackageRelation {
1363 name: Name::new("java-environment").unwrap(),
1364 version_requirement: Some(VersionRequirement {
1365 comparison: VersionComparison::GreaterOrEqual,
1366 version: "17".parse().unwrap(),
1367 }),
1368 },
1369 description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
1370 }),
1371 )]
1372 fn opt_depend_from_string(
1373 #[case] input: &str,
1374 #[case] expected_result: Result<OptionalDependency, Error>,
1375 ) {
1376 let opt_depend_result = OptionalDependency::from_str(input);
1377 assert_eq!(expected_result, opt_depend_result);
1378 }
1379
1380 #[rstest]
1381 #[case(
1382 "#invalid-name: this is an example dependency",
1383 "invalid first character of package name"
1384 )]
1385 #[case(": no_name_colon", "invalid first character of package name")]
1386 #[case(
1387 "name:description with no leading whitespace",
1388 "invalid character in package name"
1389 )]
1390 #[case(
1391 "dep-name>=10: \n\ndescription with\rnewlines",
1392 "expected no carriage returns or newlines"
1393 )]
1394 fn opt_depend_invalid_string_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
1395 let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
1396 panic!("'{input}' did not fail to parse as expected")
1397 };
1398 assert!(
1399 err_msg.contains(err_snippet),
1400 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1401 );
1402 }
1403
1404 #[rstest]
1405 #[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
1406 #[case("example.so=1.0.0-64", SonameV1::Explicit {
1407 name: "example.so".parse().unwrap(),
1408 version: "1.0.0".parse().unwrap(),
1409 architecture: ElfArchitectureFormat::Bit64,
1410 })]
1411 fn sonamev1_from_string(
1412 #[case] input: &str,
1413 #[case] expected_result: SonameV1,
1414 ) -> testresult::TestResult<()> {
1415 let soname = SonameV1::from_str(input)?;
1416 assert_eq!(expected_result, soname);
1417 assert_eq!(input, soname.to_string());
1418 Ok(())
1419 }
1420
1421 #[rstest]
1422 #[case(
1423 "libwlroots-0.18.so=libwlroots-0.18.so-64",
1424 SonameV1::Unversioned {
1425 name: "libwlroots-0.18.so".parse().unwrap(),
1426 soname: "libwlroots-0.18.so".parse().unwrap(),
1427 architecture: ElfArchitectureFormat::Bit64,
1428 },
1429 )]
1430 #[case(
1431 "libexample.so=otherlibexample.so-64",
1432 SonameV1::Unversioned {
1433 name: "libexample.so".parse().unwrap(),
1434 soname: "otherlibexample.so".parse().unwrap(),
1435 architecture: ElfArchitectureFormat::Bit64,
1436 },
1437 )]
1438 fn sonamev1_from_string_without_version(
1439 #[case] input: &str,
1440 #[case] expected_result: SonameV1,
1441 ) -> testresult::TestResult<()> {
1442 let soname = SonameV1::from_str(input)?;
1443 assert_eq!(expected_result, soname);
1444 assert_eq!(input, soname.to_string());
1445 Ok(())
1446 }
1447
1448 #[rstest]
1449 #[case("noso", "invalid shared object name")]
1450 #[case("invalidversion.so=1*2-64", "expected version or shared object name")]
1451 #[case(
1452 "nodelimiter.so=1.64",
1453 "expected a version or shared object name, followed by an ELF architecture format"
1454 )]
1455 #[case(
1456 "noarchitecture.so=1-",
1457 "expected a version or shared object name, followed by an ELF architecture format"
1458 )]
1459 #[case("invalidarchitecture.so=1-82", "invalid architecture")]
1460 #[case("invalidsoname.so~1.64", "unexpected trailing content")]
1461 fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
1462 let result = SonameV1::from_str(input);
1463 assert!(result.is_err(), "Expected SonameV1 parsing to fail");
1464 let err = result.unwrap_err();
1465 let pretty_error = err.to_string();
1466 assert!(
1467 pretty_error.contains(error_snippet),
1468 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1469 );
1470 }
1471
1472 #[rstest]
1473 #[case(
1474 "otherlibexample.so",
1475 VersionOrSoname::Soname(
1476 SharedObjectName::new("otherlibexample.so").unwrap())
1477 )]
1478 #[case(
1479 "1.0.0",
1480 VersionOrSoname::Version(
1481 PackageVersion::from_str("1.0.0").unwrap())
1482 )]
1483 fn version_or_soname_from_string(
1484 #[case] input: &str,
1485 #[case] expected_result: VersionOrSoname,
1486 ) -> testresult::TestResult<()> {
1487 let version = VersionOrSoname::from_str(input)?;
1488 assert_eq!(expected_result, version);
1489 assert_eq!(input, version.to_string());
1490 Ok(())
1491 }
1492
1493 #[rstest]
1494 #[case(
1495 "lib:libexample.so",
1496 SonameV2 {
1497 prefix: "lib".parse().unwrap(),
1498 soname: Soname {
1499 name: "libexample.so".parse().unwrap(),
1500 version: None,
1501 },
1502 },
1503 )]
1504 #[case(
1505 "usr:libexample.so.1",
1506 SonameV2 {
1507 prefix: "usr".parse().unwrap(),
1508 soname: Soname {
1509 name: "libexample.so".parse().unwrap(),
1510 version: "1".parse().ok(),
1511 },
1512 },
1513 )]
1514 #[case(
1515 "lib:libexample.so.1.2.3",
1516 SonameV2 {
1517 prefix: "lib".parse().unwrap(),
1518 soname: Soname {
1519 name: "libexample.so".parse().unwrap(),
1520 version: "1.2.3".parse().ok(),
1521 },
1522 },
1523 )]
1524 #[case(
1525 "lib:libexample.so.so.420",
1526 SonameV2 {
1527 prefix: "lib".parse().unwrap(),
1528 soname: Soname {
1529 name: "libexample.so.so".parse().unwrap(),
1530 version: "420".parse().ok(),
1531 },
1532 },
1533 )]
1534 #[case(
1535 "lib:libexample.so.test",
1536 SonameV2 {
1537 prefix: "lib".parse().unwrap(),
1538 soname: Soname {
1539 name: "libexample.so".parse().unwrap(),
1540 version: "test".parse().ok(),
1541 },
1542 },
1543 )]
1544 fn sonamev2_from_string(
1545 #[case] input: &str,
1546 #[case] expected_result: SonameV2,
1547 ) -> testresult::TestResult<()> {
1548 let soname = SonameV2::from_str(input)?;
1549 assert_eq!(expected_result, soname);
1550 assert_eq!(input, soname.to_string());
1551 Ok(())
1552 }
1553
1554 #[rstest]
1555 #[case("libexample.so.1", "invalid shared library prefix delimiter")]
1556 #[case("lib:libexample.so-abc", "invalid version delimiter")]
1557 #[case("lib:libexample.so.10-10", "invalid pkgver character")]
1558 #[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
1559 fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
1560 let result = SonameV2::from_str(input);
1561 assert!(result.is_err(), "Expected SonameV2 parsing to fail");
1562 let err = result.unwrap_err();
1563 let pretty_error = err.to_string();
1564 assert!(
1565 pretty_error.contains(error_snippet),
1566 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
1567 );
1568 }
1569
1570 #[rstest]
1571 #[case(
1572 "example",
1573 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), None))
1574 )]
1575 #[case(
1576 "example=1.0.0",
1577 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), "=1.0.0".parse().ok()))
1578 )]
1579 #[case(
1580 "example>=1.0.0",
1581 RelationOrSoname::Relation(PackageRelation::new("example".parse().unwrap(), ">=1.0.0".parse().ok()))
1582 )]
1583 #[case(
1584 "example.so",
1585 RelationOrSoname::BasicSonameV1(
1586 SonameV1::new(
1587 "example.so".parse().unwrap(),
1588 None,
1589 None,
1590 ).unwrap()
1591 )
1592 )]
1593 fn test_relation_or_soname_parser(
1594 #[case] mut input: &str,
1595 #[case] expected: RelationOrSoname,
1596 ) -> TestResult {
1597 let input_str = input.to_string();
1598 let result = RelationOrSoname::parser(&mut input)?;
1599 assert_eq!(result, expected);
1600 assert_eq!(result.to_string(), input_str);
1601 Ok(())
1602 }
1603}