use std::fmt;
use std::str::FromStr;
use crate::{Error, Result};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RustLibPath(String);
impl RustLibPath {
pub fn new(path: impl Into<String>) -> Result<Self> {
let path = path.into();
validate(&path)?;
Ok(Self(path))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for RustLibPath {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RustLibPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for RustLibPath {
type Err = Error;
fn from_str(value: &str) -> Result<Self> {
Self::new(value)
}
}
impl TryFrom<String> for RustLibPath {
type Error = Error;
fn try_from(value: String) -> Result<Self> {
Self::new(value)
}
}
impl TryFrom<&str> for RustLibPath {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
Self::new(value)
}
}
fn validate(path: &str) -> Result<()> {
let invalid = |reason| Error::InvalidRustLibPath {
path: path.to_owned(),
reason,
};
if path.is_empty() {
return Err(invalid("path is empty"));
}
if path.starts_with('/') {
return Err(invalid("absolute paths are not allowed"));
}
if path.ends_with('/') {
return Err(invalid("trailing '/' is not allowed"));
}
if path.contains('\\') {
return Err(invalid("use '/' rather than backslash as the separator"));
}
if path.contains(':') {
return Err(invalid("':' is not allowed in Rust library paths"));
}
if path.contains('\0') {
return Err(invalid("NUL is not allowed"));
}
for component in path.split('/') {
if component.is_empty() {
return Err(invalid("empty path components are not allowed"));
}
if component == "." || component == ".." {
return Err(invalid("'.' and '..' components are not allowed"));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::RustLibPath;
#[test]
fn accepts_canonical_relative_paths() {
for path in ["Cargo.toml", "src/lib.rs", ".gitignore", "资料/代码.rs"] {
assert!(RustLibPath::new(path).is_ok(), "{path}");
}
}
#[test]
fn rejects_unsafe_or_noncanonical_paths() {
for path in [
"",
"/etc/passwd",
"../outside",
"src/../outside",
"./src/lib.rs",
"src//lib.rs",
"src/lib.rs/",
"src\\lib.rs",
"C:drive-relative.rs",
"nul\0name",
] {
assert!(RustLibPath::new(path).is_err(), "{path:?}");
}
}
}