use std::{
fmt::{Display, Formatter},
path::{Path, PathBuf},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use winnow::{
ModalResult,
Parser,
combinator::{alt, cut_err, eof, peek, repeat_till},
error::{StrContext, StrContextValue},
token::{any, rest},
};
use crate::{Error, SharedLibraryPrefix};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AbsolutePath(PathBuf);
impl AbsolutePath {
pub fn new(path: PathBuf) -> Result<AbsolutePath, Error> {
match path.is_absolute() {
true => Ok(AbsolutePath(path)),
false => Err(Error::PathNotAbsolute(path)),
}
}
pub fn inner(&self) -> &Path {
&self.0
}
}
impl FromStr for AbsolutePath {
type Err = Error;
fn from_str(s: &str) -> Result<AbsolutePath, Self::Err> {
match Path::new(s).is_absolute() {
true => Ok(AbsolutePath(PathBuf::from(s))),
false => Err(Error::PathNotAbsolute(PathBuf::from(s))),
}
}
}
impl Display for AbsolutePath {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.inner().display())
}
}
pub type BuildDirectory = AbsolutePath;
pub type StartDirectory = AbsolutePath;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RelativePath(PathBuf);
impl RelativePath {
pub fn new(path: PathBuf) -> Result<RelativePath, Error> {
if !path.is_relative() {
return Err(Error::PathNotRelative(path));
}
Ok(RelativePath(path))
}
pub fn into_inner(self) -> PathBuf {
self.0
}
}
impl AsRef<Path> for RelativePath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl FromStr for RelativePath {
type Err = Error;
fn from_str(s: &str) -> Result<RelativePath, Self::Err> {
Self::new(PathBuf::from(s))
}
}
impl Display for RelativePath {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.as_ref().display())
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RelativeFilePath(PathBuf);
impl RelativeFilePath {
pub fn new(path: PathBuf) -> Result<RelativeFilePath, Error> {
if path
.to_string_lossy()
.ends_with(std::path::MAIN_SEPARATOR_STR)
{
return Err(Error::PathIsNotAFile(path));
}
if !path.is_relative() {
return Err(Error::PathNotRelative(path));
}
Ok(RelativeFilePath(path))
}
pub fn inner(&self) -> &Path {
&self.0
}
}
impl FromStr for RelativeFilePath {
type Err = Error;
fn from_str(s: &str) -> Result<RelativeFilePath, Self::Err> {
Self::new(PathBuf::from(s))
}
}
impl Display for RelativeFilePath {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.inner().display())
}
}
pub type Backup = RelativeFilePath;
pub type Install = RelativeFilePath;
pub type Changelog = RelativeFilePath;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SonameLookupDirectory {
pub prefix: SharedLibraryPrefix,
pub directory: AbsolutePath,
}
impl SonameLookupDirectory {
pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
Self { prefix, directory }
}
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 directory = rest
.verify(|s: &str| !s.is_empty())
.try_map(AbsolutePath::from_str)
.context(StrContext::Label("directory"))
.context(StrContext::Expected(StrContextValue::Description(
"directory for a shared object lookup path",
)))
.parse_next(input)?;
Ok(Self { prefix, directory })
}
}
impl Display for SonameLookupDirectory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.prefix, self.directory)
}
}
impl FromStr for SonameLookupDirectory {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case("/home", BuildDirectory::new(PathBuf::from("/home")))]
#[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
#[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
#[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
fn build_dir_from_string(#[case] s: &str, #[case] result: Result<BuildDirectory, Error>) {
assert_eq!(BuildDirectory::from_str(s), result);
}
#[rstest]
#[case("/start", StartDirectory::new(PathBuf::from("/start")))]
#[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
#[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
#[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
fn startdir_from_str(#[case] s: &str, #[case] result: Result<StartDirectory, Error>) {
assert_eq!(StartDirectory::from_str(s), result);
}
#[rstest]
#[case("etc/test.conf", RelativePath::new(PathBuf::from("etc/test.conf")))]
#[case("etc/", RelativePath::new(PathBuf::from("etc/")))]
#[case(
"/etc/test.conf",
Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
)]
#[case(
"../etc/test.conf",
RelativePath::new(PathBuf::from("../etc/test.conf"))
)]
fn relative_path_from_str(#[case] s: &str, #[case] result: Result<RelativePath, Error>) {
assert_eq!(RelativePath::from_str(s), result);
}
#[rstest]
#[case("etc/test.conf", RelativeFilePath::new(PathBuf::from("etc/test.conf")))]
#[case(
"/etc/test.conf",
Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
)]
#[case("etc/", Err(Error::PathIsNotAFile(PathBuf::from("etc/"))))]
#[case("etc", RelativeFilePath::new(PathBuf::from("etc")))]
#[case(
"../etc/test.conf",
RelativeFilePath::new(PathBuf::from("../etc/test.conf"))
)]
fn relative_file_path_from_str(
#[case] s: &str,
#[case] result: Result<RelativeFilePath, Error>,
) {
assert_eq!(RelativeFilePath::from_str(s), result);
}
#[rstest]
#[case("lib:/usr/lib", SonameLookupDirectory {
prefix: "lib".parse()?,
directory: AbsolutePath::from_str("/usr/lib")?,
})]
#[case("lib32:/usr/lib32", SonameLookupDirectory {
prefix: "lib32".parse()?,
directory: AbsolutePath::from_str("/usr/lib32")?,
})]
fn soname_lookup_directory_from_string(
#[case] input: &str,
#[case] expected_result: SonameLookupDirectory,
) -> TestResult {
let lookup_directory = SonameLookupDirectory::from_str(input)?;
assert_eq!(expected_result, lookup_directory);
assert_eq!(input, lookup_directory.to_string());
Ok(())
}
#[rstest]
#[case("lib", "invalid shared library prefix delimiter")]
#[case("lib:", "invalid directory")]
#[case(":/usr/lib", "invalid first character of package name")]
fn invalid_soname_lookup_directory_parser(#[case] input: &str, #[case] error_snippet: &str) {
let result = SonameLookupDirectory::from_str(input);
assert!(result.is_err(), "Expected LookupDirectory 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}"
);
}
}