I've implemented a custom trait on std::path::Path
(and by extension std::path::PathBuf
) with a method which turns the struct into a file url and a trait on std::path::PathBuf
which constructs a PathBuf from a file url.
Would love some feedback on the code, especially any violations of naming conventions, best practices (should I seal those traits or make something that currently isn't generic?) or unnecessary allocations, I'm pretty new to Rust.
Also (of course) if there's some edge case in the logic I'm failing to consider please let me know!
use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use std::string::FromUtf8Error;
use lazy_static::lazy_static;
use regex::Regex;
use urlencoding::{decode, encode};
lazy_static! {
// We don't want to percent encode the colon on a Windows drive letter.
static ref WINDOWS_DRIVE: Regex = Regex::new(r"[a-zA-Z]:").unwrap();
static ref SEPARATOR: Regex = Regex::new(r"[/\\]").unwrap();
}
static FORWARD_SLASH: &str = "/";
#[derive(Debug)]
pub struct Utf8DecodeError {
details: String,
}
impl Utf8DecodeError {
fn new(msg: &str) -> Utf8DecodeError {
Utf8DecodeError {
details: msg.to_string(),
}
}
}
impl fmt::Display for Utf8DecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for Utf8DecodeError {
fn description(&self) -> &str {
&self.details
}
}
pub fn encode_file_component(path_part: &str) -> String {
// If it's a separator char or a Windows drive return
// as-is.
if SEPARATOR.is_match(path_part) || WINDOWS_DRIVE.is_match(path_part) {
path_part.to_owned()
} else {
encode(path_part).to_string()
}
}
pub fn file_url_to_pathbuf(file_url: &str) -> Result<PathBuf, FromUtf8Error> {
SEPARATOR
.split(file_url)
.enumerate()
.map(|(i, url_piece)| {
if i == 0 && url_piece == "file:" {
// File url should always be abspath
Ok(String::from(FORWARD_SLASH))
} else {
let dec_str = decode(url_piece)?;
Ok(dec_str.into_owned())
}
})
.collect()
}
pub trait PathFileUrlExt {
fn to_file_url(&self) -> Result<String, Utf8DecodeError>;
}
pub trait PathFromFileUrlExt {
fn from_file_url(file_url: &str) -> Result<PathBuf, FromUtf8Error>;
}
// NOTE: Because PathBuf implements Deref to Path, this also gets
// implemented for PathBuf.
impl PathFileUrlExt for Path {
fn to_file_url(&self) -> Result<String, Utf8DecodeError> {
let path_parts: Result<PathBuf, Utf8DecodeError> = self
.components()
.map(|part| match part.as_os_str().to_str() {
Some(part) => Ok(encode_file_component(part)),
None => Err(Utf8DecodeError::new("File path not UTF-8 compatible!")),
})
.collect();
let parts = path_parts?;
let path_str = parts
.to_str()
.ok_or_else(|| Utf8DecodeError::new("File path not UTF-8 compatible!"))?;
Ok(format!("file://{}", path_str))
}
}
impl PathFromFileUrlExt for PathBuf {
fn from_file_url(file_url: &str) -> Result<PathBuf, FromUtf8Error> {
file_url_to_pathbuf(file_url)
}
}
&'static str
- saves some allocations \$\endgroup\$url
crate. Converting from/to it seems more natural (than &str) \$\endgroup\$