use crate::error::{self, Result};
use path_absolutize::Absolutize;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use snafu::{ensure, OptionExt, ResultExt};
use std::convert::TryFrom;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct TargetName {
raw: String,
resolved: Option<String>,
}
impl TargetName {
pub fn new<S: Into<String>>(raw: S) -> Result<Self> {
let raw = raw.into();
let resolved = clean_name(&raw)?;
if raw == resolved {
Ok(Self {
raw,
resolved: None,
})
} else {
Ok(Self {
raw,
resolved: Some(resolved),
})
}
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn resolved(&self) -> &str {
match &self.resolved {
None => self.raw(),
Some(resolved) => resolved,
}
}
}
impl FromStr for TargetName {
type Err = crate::error::Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl TryFrom<String> for TargetName {
type Error = crate::error::Error;
fn try_from(value: String) -> Result<Self> {
TargetName::new(value)
}
}
impl TryFrom<&str> for TargetName {
type Error = crate::error::Error;
fn try_from(value: &str) -> Result<Self> {
TargetName::new(value)
}
}
impl Serialize for TargetName {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.raw())
}
}
impl<'de> Deserialize<'de> for TargetName {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
TargetName::new(s).map_err(|e| D::Error::custom(format!("{}", e)))
}
}
fn clean_name(name: &str) -> Result<String> {
ensure!(name != "..", error::UnsafeTargetNameDotDotSnafu);
ensure!(!name.is_empty(), error::UnsafeTargetNameEmptySnafu { name });
let name_path = PathBuf::from(name);
let absolute = name_path.is_absolute();
let clean = {
let proposed = name_path
.absolutize_from(&PathBuf::from("/"))
.context(error::TargetNameResolveSnafu { name })?;
if absolute {
proposed.to_path_buf()
} else {
let mut components = proposed.components();
let first_component = components
.next()
.context(error::TargetNameComponentsEmptySnafu { name })?
.as_os_str();
ensure!(
first_component == "/",
error::TargetNameRootMissingSnafu { name }
);
components.as_path().to_owned()
}
};
let final_name = clean
.as_os_str()
.to_str()
.context(error::PathUtf8Snafu { path: &clean })?
.to_string();
ensure!(
!final_name.is_empty(),
error::UnsafeTargetNameEmptySnafu { name }
);
ensure!(
final_name != "/",
error::UnsafeTargetNameSlashSnafu { name }
);
Ok(final_name)
}
#[test]
fn simple_1() {
let name = "/absolute/path/is/ok.txt";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn simple_2() {
let name = "relative/path/is/ok.txt";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn simple_3() {
let name = "not-path-like.txt";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn resolved_1() {
let name = "/this/../is/ok.txt";
let actual = clean_name(name).unwrap();
let expected = "/is/ok.txt";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_2() {
let name = "../x";
let actual = clean_name(name).unwrap();
let expected = "x";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_3() {
let name = "../../x";
let actual = clean_name(name).unwrap();
let expected = "x";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_4() {
let name = "/../x";
let actual = clean_name(name).unwrap();
let expected = "/x";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_5() {
let name = "/../../x";
let actual = clean_name(name).unwrap();
let expected = "/x";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_6() {
let name = "/this/../../../../is/ok.txt";
let actual = clean_name(name).unwrap();
let expected = "/is/ok.txt";
assert_eq!(expected, &actual);
}
#[test]
fn resolved_7() {
let name = "foo";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn resolved_8() {
let name = "/foo";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn uncleaned_1() {
let name = r#"~/\.\."#;
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn uncleaned_2() {
let name = r#"funky\/\.\.\/name"#;
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn uncleaned_3() {
let name = "/weird/\\..\\/path";
let actual = clean_name(name).unwrap();
let expected = name;
assert_eq!(expected, &actual);
}
#[test]
fn bad_1() {
let name = "..";
let error = clean_name(name).err().unwrap();
assert!(matches!(error, error::Error::UnsafeTargetNameDotDot { .. }));
}
#[test]
fn bad_2() {
let name = "../";
let error = clean_name(name).err().unwrap();
assert!(matches!(error, error::Error::UnsafeTargetNameEmpty { .. }));
}
#[test]
fn bad_3() {
let name = "/..";
let error = clean_name(name).err().unwrap();
assert!(matches!(error, error::Error::UnsafeTargetNameSlash { .. }));
}