use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use winnow::{
ModalResult,
Parser,
ascii::digit1,
combinator::{alt, cut_err, eof, fail, peek, repeat, repeat_till},
error::{StrContext, StrContextValue},
stream::Stream,
token::{any, rest, take_while},
};
#[cfg(doc)]
use crate::PackageRelation;
use crate::{ElfArchitectureFormat, Error, Name, PackageVersion, SharedObjectName};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VersionOrSoname {
Version(PackageVersion),
Soname(SharedObjectName),
}
impl FromStr for VersionOrSoname {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl VersionOrSoname {
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let checkpoint = input.checkpoint();
let soname_result = SharedObjectName::parser.parse_next(input);
if soname_result.is_ok() {
let soname = soname_result?;
return Ok(VersionOrSoname::Soname(soname));
}
input.reset(&checkpoint);
let version_result = rest.and_then(PackageVersion::parser).parse_next(input);
if version_result.is_ok() {
let version = version_result?;
return Ok(VersionOrSoname::Version(version));
}
cut_err(fail)
.context(StrContext::Expected(StrContextValue::Description(
"version or shared object name",
)))
.parse_next(input)
}
}
impl Display for VersionOrSoname {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
VersionOrSoname::Version(version) => write!(f, "{version}"),
VersionOrSoname::Soname(soname) => write!(f, "{soname}"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum SonameV1 {
Basic(SharedObjectName),
Unversioned {
name: SharedObjectName,
soname: SharedObjectName,
architecture: ElfArchitectureFormat,
},
Explicit {
name: SharedObjectName,
version: PackageVersion,
architecture: ElfArchitectureFormat,
},
}
impl SonameV1 {
pub fn new(
name: SharedObjectName,
version_or_soname: Option<VersionOrSoname>,
architecture: Option<ElfArchitectureFormat>,
) -> Result<Self, Error> {
match (version_or_soname, architecture) {
(None, None) => Ok(Self::Basic(name)),
(Some(VersionOrSoname::Version(version)), Some(architecture)) => Ok(Self::Explicit {
name,
version,
architecture,
}),
(Some(VersionOrSoname::Soname(soname)), Some(architecture)) => Ok(Self::Unversioned {
name,
soname,
architecture,
}),
(None, Some(_)) => Err(Error::InvalidSonameV1(
"SonameV1 needs a version when specifying architecture",
)),
(Some(_), None) => Err(Error::InvalidSonameV1(
"SonameV1 needs an architecture when specifying version",
)),
}
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let name = Self::parse_shared_object_name(input)?;
if Self::parse_version_delimiter(input).is_err() {
return Ok(SonameV1::Basic(name));
}
let (raw_version_or_soname, _): (String, _) =
cut_err(repeat_till(1.., any, peek(("-", digit1, eof))))
.context(StrContext::Expected(StrContextValue::Description(
"a version or shared object name, followed by an ELF architecture format",
)))
.parse_next(input)?;
let version_or_soname =
VersionOrSoname::parser.parse_next(&mut raw_version_or_soname.as_str())?;
Self::parse_architecture_delimiter(input)?;
let architecture = Self::parse_architecture(input)?;
match version_or_soname {
VersionOrSoname::Version(version) => Ok(SonameV1::Explicit {
name,
version,
architecture,
}),
VersionOrSoname::Soname(soname) => Ok(SonameV1::Unversioned {
name,
soname,
architecture,
}),
}
}
fn parse_shared_object_name(input: &mut &str) -> ModalResult<SharedObjectName> {
repeat_till(1.., any, peek(alt(("=", eof))))
.try_map(|(name, _): (String, &str)| SharedObjectName::from_str(&name))
.context(StrContext::Label("shared object name"))
.parse_next(input)
}
fn parse_version_delimiter(input: &mut &str) -> ModalResult<()> {
cut_err("=")
.context(StrContext::Label("version delimiter"))
.context(StrContext::Expected(StrContextValue::Description(
"version delimiter `=`",
)))
.parse_next(input)
.map(|_| ())
}
fn parse_architecture_delimiter(input: &mut &str) -> ModalResult<()> {
cut_err("-")
.context(StrContext::Label("architecture delimiter"))
.context(StrContext::Expected(StrContextValue::Description(
"architecture delimiter `-`",
)))
.parse_next(input)
.map(|_| ())
}
fn parse_architecture(input: &mut &str) -> ModalResult<ElfArchitectureFormat> {
cut_err(take_while(1.., |c: char| c.is_ascii_digit()))
.try_map(ElfArchitectureFormat::from_str)
.context(StrContext::Label("architecture"))
.parse_next(input)
}
pub fn shared_object_name(&self) -> &SharedObjectName {
match self {
SonameV1::Basic(name) => name,
SonameV1::Unversioned { name, .. } => name,
SonameV1::Explicit { name, .. } => name,
}
}
}
impl FromStr for SonameV1 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for SonameV1 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Basic(name) => write!(f, "{name}"),
Self::Unversioned {
name,
soname,
architecture,
} => write!(f, "{name}={soname}-{architecture}"),
Self::Explicit {
name,
version,
architecture,
} => write!(f, "{name}={version}-{architecture}"),
}
}
}
pub type SharedLibraryPrefix = Name;
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Soname {
pub name: SharedObjectName,
pub version: Option<PackageVersion>,
}
impl Soname {
pub fn new(name: SharedObjectName, version: Option<PackageVersion>) -> Self {
Self { name, version }
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let name = cut_err(
(
repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof)))),
cut_err(repeat::<_, _, String, _, _>(1.., ".so"))
.context(StrContext::Label("suffix"))
.context(StrContext::Expected(StrContextValue::Description(
"shared object name suffix '.so'",
))),
)
.take()
.and_then(Name::parser)
.map(SharedObjectName),
)
.context(StrContext::Label("shared object name"))
.parse_next(input)?;
let delimiter = cut_err(alt((".", eof)))
.context(StrContext::Label("version delimiter"))
.context(StrContext::Expected(StrContextValue::Description(
"version delimiter `.`",
)))
.parse_next(input)?;
let version = match delimiter {
"" => None,
"." => Some(rest.and_then(PackageVersion::parser).parse_next(input)?),
_ => unreachable!(),
};
Ok(Self { name, version })
}
}
impl Display for Soname {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.version {
Some(version) => write!(f, "{name}.{version}", name = self.name),
None => write!(f, "{name}", name = self.name),
}
}
}
impl FromStr for Soname {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SonameV2 {
pub prefix: SharedLibraryPrefix,
pub soname: Soname,
}
impl SonameV2 {
pub fn new(prefix: SharedLibraryPrefix, soname: Soname) -> Self {
Self { prefix, soname }
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let prefix = cut_err(
repeat_till(1.., any, peek(alt((":", eof))))
.try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
)
.context(StrContext::Label("prefix for a shared object lookup path"))
.parse_next(input)?;
cut_err(":")
.context(StrContext::Label("shared library prefix delimiter"))
.context(StrContext::Expected(StrContextValue::Description(
"shared library prefix `:`",
)))
.parse_next(input)?;
let soname = Soname::parser.parse_next(input)?;
Ok(Self { prefix, soname })
}
}
impl FromStr for SonameV2 {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for SonameV2 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{prefix}:{soname}",
prefix = self.prefix,
soname = self.soname
)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("example.so", SonameV1::Basic("example.so".parse().unwrap()))]
#[case("example.so=1.0.0-64", SonameV1::Explicit {
name: "example.so".parse().unwrap(),
version: "1.0.0".parse().unwrap(),
architecture: ElfArchitectureFormat::Bit64,
})]
fn sonamev1_from_string(
#[case] input: &str,
#[case] expected_result: SonameV1,
) -> testresult::TestResult<()> {
let soname = SonameV1::from_str(input)?;
assert_eq!(expected_result, soname);
assert_eq!(input, soname.to_string());
Ok(())
}
#[rstest]
#[case(
"libwlroots-0.18.so=libwlroots-0.18.so-64",
SonameV1::Unversioned {
name: "libwlroots-0.18.so".parse().unwrap(),
soname: "libwlroots-0.18.so".parse().unwrap(),
architecture: ElfArchitectureFormat::Bit64,
},
)]
#[case(
"libexample.so=otherlibexample.so-64",
SonameV1::Unversioned {
name: "libexample.so".parse().unwrap(),
soname: "otherlibexample.so".parse().unwrap(),
architecture: ElfArchitectureFormat::Bit64,
},
)]
fn sonamev1_from_string_without_version(
#[case] input: &str,
#[case] expected_result: SonameV1,
) -> testresult::TestResult<()> {
let soname = SonameV1::from_str(input)?;
assert_eq!(expected_result, soname);
assert_eq!(input, soname.to_string());
Ok(())
}
#[rstest]
#[case("noso", "invalid shared object name")]
#[case("invalidversion.so=1🐀2-64", "expected version or shared object name")]
#[case(
"nodelimiter.so=1.64",
"expected a version or shared object name, followed by an ELF architecture format"
)]
#[case(
"noarchitecture.so=1-",
"expected a version or shared object name, followed by an ELF architecture format"
)]
#[case("invalidarchitecture.so=1-82", "invalid architecture")]
#[case("invalidsoname.so~1.64", "unexpected trailing content")]
fn invalid_sonamev1_parser(#[case] input: &str, #[case] error_snippet: &str) {
let result = SonameV1::from_str(input);
assert!(result.is_err(), "Expected SonameV1 parsing to fail");
let err = result.unwrap_err();
let pretty_error = err.to_string();
assert!(
pretty_error.contains(error_snippet),
"Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
);
}
#[rstest]
#[case(
"otherlibexample.so",
VersionOrSoname::Soname(
SharedObjectName::new("otherlibexample.so").unwrap())
)]
#[case(
"1.0.0",
VersionOrSoname::Version(
PackageVersion::from_str("1.0.0").unwrap())
)]
fn version_or_soname_from_string(
#[case] input: &str,
#[case] expected_result: VersionOrSoname,
) -> testresult::TestResult<()> {
let version = VersionOrSoname::from_str(input)?;
assert_eq!(expected_result, version);
assert_eq!(input, version.to_string());
Ok(())
}
#[rstest]
#[case(
"lib:libexample.so",
SonameV2 {
prefix: "lib".parse().unwrap(),
soname: Soname {
name: "libexample.so".parse().unwrap(),
version: None,
},
},
)]
#[case(
"usr:libexample.so.1",
SonameV2 {
prefix: "usr".parse().unwrap(),
soname: Soname {
name: "libexample.so".parse().unwrap(),
version: "1".parse().ok(),
},
},
)]
#[case(
"lib:libexample.so.1.2.3",
SonameV2 {
prefix: "lib".parse().unwrap(),
soname: Soname {
name: "libexample.so".parse().unwrap(),
version: "1.2.3".parse().ok(),
},
},
)]
#[case(
"lib:libexample.so.so.420",
SonameV2 {
prefix: "lib".parse().unwrap(),
soname: Soname {
name: "libexample.so.so".parse().unwrap(),
version: "420".parse().ok(),
},
},
)]
#[case(
"lib:libexample.so.test",
SonameV2 {
prefix: "lib".parse().unwrap(),
soname: Soname {
name: "libexample.so".parse().unwrap(),
version: "test".parse().ok(),
},
},
)]
fn sonamev2_from_string(
#[case] input: &str,
#[case] expected_result: SonameV2,
) -> testresult::TestResult<()> {
let soname = SonameV2::from_str(input)?;
assert_eq!(expected_result, soname);
assert_eq!(input, soname.to_string());
Ok(())
}
#[rstest]
#[case("libexample.so.1", "invalid shared library prefix delimiter")]
#[case("lib:libexample.so-abc", "invalid version delimiter")]
#[case("lib:libexample.so.10-10", "invalid pkgver character")]
#[case("lib:libexample.so.1.0.0-64", "invalid pkgver character")]
fn invalid_sonamev2_parser(#[case] input: &str, #[case] error_snippet: &str) {
let result = SonameV2::from_str(input);
assert!(result.is_err(), "Expected SonameV2 parsing to fail");
let err = result.unwrap_err();
let pretty_error = err.to_string();
assert!(
pretty_error.contains(error_snippet),
"Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
);
}
}