use std::{
fmt::{Display, Formatter},
path::{Path, PathBuf},
str::FromStr,
};
use url::Url;
use crate::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filename {
inner: PathBuf,
}
impl Filename {
pub fn new(s: String) -> Result<Self, Error> {
if s.is_empty() || s.contains([std::path::MAIN_SEPARATOR, '\0']) {
Err(Error::InvalidFilename(s))
} else {
Ok(Self { inner: s.into() })
}
}
pub fn as_str(&self) -> &str {
self.inner.as_os_str().to_str().unwrap()
}
pub fn into_string(self) -> String {
self.inner.into_os_string().into_string().unwrap()
}
pub fn inner(&self) -> &Path {
&self.inner
}
pub fn into_inner(self) -> PathBuf {
self.inner
}
}
impl FromStr for Filename {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceLocation {
File(Filename),
Url(Url),
}
impl SourceLocation {
pub fn new(s: &str) -> Result<Self, Error> {
match s.parse() {
Ok(url) => Ok(Self::Url(url)),
Err(url::ParseError::RelativeUrlWithoutBase) => {
Filename::new(s.to_owned()).map(Self::File)
}
Err(_e) => Err(Error::InvalidUrl(s.to_owned())),
}
}
}
impl FromStr for SourceLocation {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
pub filename: Option<Filename>,
pub location: SourceLocation,
}
impl Source {
pub fn new(s: &str) -> Result<Self, Error> {
if let Some((filename, loc)) = s.split_once("::") {
Ok(Self {
filename: Some(filename.parse()?),
location: loc.parse()?,
})
} else {
Ok(Self {
filename: None,
location: s.parse()?,
})
}
}
}
impl FromStr for Source {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl Display for Source {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(filename) = &self.filename {
write!(f, "{}::", filename.as_str())?;
}
match &self.location {
SourceLocation::File(file) => write!(f, "{}", file.as_str()),
SourceLocation::Url(u) => write!(f, "{u}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("bikeshed_colour.patch", Ok(Filename {
inner: PathBuf::from("bikeshed_colour.patch"),
}))]
#[case("c:foo", Ok(Filename {
inner: PathBuf::from("c:foo"),
}))]
#[case("./bikeshed_colour.patch", Err(Error::InvalidFilename("./bikeshed_colour.patch".to_owned())))]
#[case("", Err(Error::InvalidFilename("".to_owned())))]
#[case("with\0null", Err(Error::InvalidFilename("with\0null".to_owned())))]
fn parse_filename(#[case] input: &str, #[case] expected: Result<Filename, Error>) {
let filename = input.parse();
assert_eq!(filename, expected);
if let Ok(filename) = filename {
assert_eq!(filename.as_str(), input);
}
}
#[rstest]
#[case("bikeshed_colour.patch", Ok(Source {
filename: None,
location: SourceLocation::File("bikeshed_colour.patch".parse().unwrap()),
}))]
#[case("renamed::local", Ok(Source {
filename: Some("renamed".parse().unwrap()),
location: SourceLocation::File("local".parse().unwrap()),
}))]
#[case("foo-1.2.3.tar.gz::https://example.com/download", Ok(Source {
filename: Some("foo-1.2.3.tar.gz".parse().unwrap()),
location: SourceLocation::Url(Url::parse("https://example.com/download").unwrap()),
}))]
#[case("my-git-repo::git+https://example.com/project/repo.git#commit=deadbeef?signed", Ok(Source {
filename: Some("my-git-repo".parse().unwrap()),
location: SourceLocation::Url(Url::parse("git+https://example.com/project/repo.git#commit=deadbeef?signed").unwrap()),
}))]
#[case("file:///somewhere/else", Ok(Source {
filename: None,
location: SourceLocation::Url(Url::parse("file:///somewhere/else").unwrap()),
}))]
#[case("/absolute/path", Err(Error::InvalidFilename("/absolute/path".to_owned())))]
#[case("foo:::/absolute/path", Err(Error::InvalidFilename(":/absolute/path".to_owned())))]
fn parse_source(#[case] input: &str, #[case] expected: Result<Source, Error>) {
let source = input.parse();
assert_eq!(source, expected);
if let Ok(source) = source {
assert_eq!(source.to_string(), input);
}
}
}