use std::{
env::var,
ffi::OsStr,
fmt,
ops::Deref,
path::{Path, PathBuf},
};
use serde::Deserialize;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct TildePathBuf {
path: PathBuf,
}
impl TildePathBuf {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl From<TildePathBuf> for PathBuf {
fn from(value: TildePathBuf) -> Self {
value.path.clone()
}
}
impl From<PathBuf> for TildePathBuf {
fn from(value: PathBuf) -> Self {
Self::new(value)
}
}
impl From<&Path> for TildePathBuf {
fn from(value: &Path) -> Self {
Self::new(value.into())
}
}
impl AsRef<Path> for TildePathBuf {
fn as_ref(&self) -> &Path {
self.path()
}
}
impl Deref for TildePathBuf {
type Target = Path;
fn deref(&self) -> &Self::Target {
self.path()
}
}
impl<'de> Deserialize<'de> for TildePathBuf {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct TildeExpandedVisitor;
impl serde::de::Visitor<'_> for TildeExpandedVisitor {
type Value = TildePathBuf;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a path, with optional tildes to expand")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let expander = Expander::new().map_err(|e| E::custom(e))?;
Ok(TildePathBuf::new(expander.expand(Path::new(value))))
}
}
deserializer.deserialize_str(TildeExpandedVisitor)
}
}
struct Expander {
home: PathBuf,
}
impl Expander {
fn new() -> Result<Self, TildeError> {
let home = var("HOME").map_err(TildeError::NoHome)?;
Ok(Self {
home: PathBuf::from(home),
})
}
#[cfg(test)]
fn with_home(home: &Path) -> Self {
Self { home: home.into() }
}
fn expand(&self, path: &Path) -> PathBuf {
let os = path.as_os_str();
let bytes = os.as_encoded_bytes();
let path: PathBuf = if bytes == b"~" {
self.home.clone()
} else if let Some(suffix) = bytes.strip_prefix(b"~/") {
unsafe {
let suffix = OsStr::from_encoded_bytes_unchecked(suffix);
self.home.join(Path::new(suffix))
}
} else {
path.to_path_buf()
};
path
}
}
#[derive(Debug, thiserror::Error)]
pub enum TildeError {
#[error("the HOME environment variable is not available")]
NoHome(#[source] std::env::VarError),
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn no_tilde() -> Result<(), Box<dyn std::error::Error>> {
let expander = Expander::with_home(Path::new("/home/foo"));
assert_eq!(expander.expand(Path::new("foo")), PathBuf::from("foo"));
Ok(())
}
#[test]
fn just_tilde() -> Result<(), Box<dyn std::error::Error>> {
let expander = Expander::with_home(Path::new("/home/foo"));
assert_eq!(expander.expand(Path::new("~")), PathBuf::from("/home/foo"));
Ok(())
}
#[test]
fn tilde_slash() -> Result<(), Box<dyn std::error::Error>> {
let expander = Expander::with_home(Path::new("/home/foo"));
assert_eq!(
expander.expand(Path::new("~/bar")),
PathBuf::from("/home/foo/bar")
);
Ok(())
}
#[test]
fn tilde_username() -> Result<(), Box<dyn std::error::Error>> {
let expander = Expander::with_home(Path::new("/home/foo"));
assert_eq!(
expander.expand(Path::new("~foo/bar")),
PathBuf::from("~foo/bar")
);
Ok(())
}
#[test]
fn tilde_slash_with_home_from_var() -> Result<(), Box<dyn std::error::Error>> {
if var("HOME").is_ok() {
let expander = Expander::new()?;
let path = expander.expand(Path::new("~/foo"));
let home = std::env::var("HOME")?;
assert_eq!(path.display().to_string(), format!("{home}/foo"));
}
Ok(())
}
#[test]
fn deser() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Deserialize)]
pub struct MyThing {
pub location: TildePathBuf,
}
let thing = r#"
location: ~/foo
"#;
if var("HOME").is_ok() {
let mything: MyThing = serde_norway::from_str(thing)?;
let home = std::env::var("HOME")?;
let wanted = PathBuf::from(format!("{home}/foo"));
assert_eq!(mything.location.path(), wanted);
}
Ok(())
}
#[test]
fn eq() {
let foo1 = TildePathBuf::new(PathBuf::from("foo"));
let foo2 = TildePathBuf::new(PathBuf::from("foo"));
let bar = TildePathBuf::new(PathBuf::from("bar"));
assert_eq!(foo1, foo2);
assert_ne!(foo1, bar);
}
#[test]
fn as_ref() {
let foo = TildePathBuf::new(PathBuf::from("foo"));
let bar: &Path = foo.as_ref();
assert_eq!(foo.path(), bar);
}
#[test]
fn deref() {
let foo = TildePathBuf::new(PathBuf::from("foo"));
let bar: &Path = foo.deref();
assert_eq!(foo.path(), bar);
}
#[test]
fn to_path_buf() {
let foo = TildePathBuf::new(PathBuf::from("foo"));
let bar = foo.to_path_buf();
assert_eq!(foo.path(), bar);
}
#[test]
fn pathbuf_from() {
let foo = TildePathBuf::new(PathBuf::from("foo"));
let bar = PathBuf::from(foo.clone());
assert_eq!(foo.path(), bar);
}
}