2
\$\begingroup\$

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)
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ This looks awesome! Did you publish this in crates? Small nit is: make the errors contain a &'static str - saves some allocations \$\endgroup\$ Commented Aug 7, 2023 at 15:05
  • 1
    \$\begingroup\$ Also, There is a fairly standard url crate. Converting from/to it seems more natural (than &str) \$\endgroup\$ Commented Aug 7, 2023 at 15:08
  • 1
    \$\begingroup\$ @JorgeLeitao I ended up using the url crate you mentioned. You can see the code it was for/from here github.com/jasmith79/playlistrs/blob/… \$\endgroup\$ Commented Aug 7, 2023 at 17:10

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.