path_encoding_utils 0.1.0

Utilities for path encoding
Documentation

use std::path::{Path, PathBuf};
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};

const FILENAME_ENCODE_SET: &AsciiSet = &CONTROLS
    .add(b'/')
    .add(b'\\')
    .add(b'?')
    .add(b'%')
    .add(b'*')
    .add(b':')
    .add(b'|')
    .add(b'"')
    .add(b'<')
    .add(b'>')
    .add(b'&')
    .add(b'=');

    /// Get the path from a url.
pub fn get_path_from_url(url: &str) -> &str {
    if let Some(start_pos) = url.find("//") {
        let mut pos = start_pos + 2;

        if let Some(third_slash_pos) = url[pos..].find('/') {
            pos += third_slash_pos;
            return &url[pos..];
        }
    }
    "/"
}

/// Generate the filename for a url.
pub fn generate_file_name(url: &str) -> String {
    let path_without_query = url.split('?').next().unwrap_or("");
    let ext = Path::new(path_without_query)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");

    let encoded_name = utf8_percent_encode(url, FILENAME_ENCODE_SET).to_string();

    // Avoid adding double extension
    if path_without_query.ends_with(&format!(".{ext}")) {
        encoded_name
    } else if ext.is_empty() {
        encoded_name
    } else {
        format!("{encoded_name}.{ext}")
    }
}

/// Convert a URL string into a sharded file path.
pub fn url_to_path_manual(base: &str, url: &str) -> PathBuf {
    // Strip protocol
    let url = url
        .trim_start_matches("http://")
        .trim_start_matches("https://");

    // Split into domain + path
    let mut parts = url.splitn(2, '/');
    let domain = parts.next().unwrap_or("");
    let path = parts.next().unwrap_or("");

    // Sanitize and join
    let mut full_path = PathBuf::from(base);
    full_path.push(domain);

    for segment in Path::new(path).components() {
        let cleaned = generate_file_name(segment.as_os_str().to_string_lossy().as_ref());
        full_path.push(cleaned);
    }

    full_path
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_path_from_url() {
        assert_eq!(get_path_from_url("https://example.com/assets/file.js"), "/assets/file.js");
        assert_eq!(get_path_from_url("http://example.com/"), "/");
        assert_eq!(get_path_from_url("ftp://example.com/path/to/resource"), "/path/to/resource");
        assert_eq!(get_path_from_url("invalid-url"), "/");
    }

    #[test]
    fn test_generate_file_name() {
        assert_eq!(
            generate_file_name("https://example.com/assets/file.js"),
            "https%3A%2F%2Fexample.com%2Fassets%2Ffile.js"
        );

        assert_eq!(
            generate_file_name("https://example.com/file?version=1.2"),
            "https%3A%2F%2Fexample.com%2Ffile%3Fversion%3D1.2"
        );

        assert_eq!(
            generate_file_name("https://example.com/file.with.double.ext.tar.gz"),
            "https%3A%2F%2Fexample.com%2Ffile.with.double.ext.tar.gz"
        );
    }

    #[test]
    fn test_url_to_path_manual() {
        let base = "/tmp/assets";

        let path = url_to_path_manual(base, "https://example.com/a/b/c.js");
        assert_eq!(
            path,
            PathBuf::from(format!(
                "{}/example.com/a/b/c.js",
                base
            ))
        );

        let path = url_to_path_manual(base, "http://cdn.example.com/lib/jquery.min.js?ver=3.6.0");
        assert_eq!(
            path,
            PathBuf::from(format!(
                "{}/cdn.example.com/lib/jquery.min.js%3Fver%3D3.6.0",
                base
            ))
        );

        let path = url_to_path_manual(base, "https://cdn.example.com/");
        assert_eq!(path, PathBuf::from(format!("{}/cdn.example.com", base)));
    }
}