use std::borrow::Cow;
use std::path::{Component, Path};
pub fn path_to_url(relative: &Path) -> String {
let mut url = String::new();
for component in relative.components() {
let piece: Cow<'_, str> = match component {
Component::Normal(name) => name.to_string_lossy(),
Component::ParentDir => Cow::Borrowed(".."),
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
};
if !url.is_empty() {
url.push('/');
}
url.push_str(&piece);
}
url
}
pub fn is_external_url(url: &str) -> bool {
if url.starts_with("//") {
return true;
}
if url.starts_with(['/', '#', '?']) {
return false;
}
url_scheme(url).is_some()
}
fn url_scheme(url: &str) -> Option<&str> {
let colon = url.find(':')?;
let scheme = &url[..colon];
if scheme.len() < 2 {
return None;
}
let mut chars = scheme.chars();
let starts_with_alpha = chars.next().is_some_and(|c| c.is_ascii_alphabetic());
let rest_is_scheme_chars =
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
(starts_with_alpha && rest_is_scheme_chars).then_some(scheme)
}
pub fn serialize_as_url<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&path_to_url(path))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn native(parts: &[&str]) -> PathBuf {
parts.iter().collect()
}
#[test]
fn test_empty_path_is_empty_url() {
assert_eq!(path_to_url(Path::new("")), "");
}
#[test]
fn test_single_component() {
assert_eq!(path_to_url(Path::new("README.md")), "README.md");
}
#[test]
fn test_nested_path_uses_forward_slashes() {
let path = native(&["docs", "guide", "index.md"]);
let url = path_to_url(&path);
assert_eq!(url, "docs/guide/index.md");
assert!(
!url.contains('\\'),
"URL must never contain a backslash, got {url}"
);
}
#[test]
fn test_deeply_nested_path() {
let path = native(&["a", "b", "c", "d", "e", "f.png"]);
assert_eq!(path_to_url(&path), "a/b/c/d/e/f.png");
}
#[test]
fn test_parent_dir_is_preserved() {
let path = native(&["..", "..", "images", "photo.png"]);
assert_eq!(path_to_url(&path), "../../images/photo.png");
}
#[test]
fn test_current_dir_is_dropped() {
assert_eq!(path_to_url(Path::new(".")), "");
let path = native(&[".", "docs", "guide.md"]);
assert_eq!(path_to_url(&path), "docs/guide.md");
}
#[test]
fn test_root_component_is_dropped() {
assert_eq!(path_to_url(Path::new("/docs/guide.md")), "docs/guide.md");
}
#[test]
fn test_spaces_and_unicode_preserved() {
let path = native(&["my images", "café \u{1f600}.png"]);
assert_eq!(path_to_url(&path), "my images/café \u{1f600}.png");
}
#[test]
fn test_no_trailing_or_leading_slash() {
let url = path_to_url(&native(&["docs", "guide.md"]));
assert!(!url.starts_with('/'));
assert!(!url.ends_with('/'));
}
#[cfg(windows)]
#[test]
fn test_windows_backslash_literal_is_converted() {
assert_eq!(path_to_url(Path::new(r"docs\guide.md")), "docs/guide.md");
assert_eq!(
path_to_url(Path::new(r"C:\notes\docs\guide.md")),
"notes/docs/guide.md"
);
}
#[cfg(unix)]
#[test]
fn test_unix_backslash_is_a_filename_character() {
assert_eq!(path_to_url(Path::new(r"weird\name.md")), r"weird\name.md");
}
#[test]
fn test_is_external_url_table() {
let cases: &[(&str, bool)] = &[
("ftp://ftp.example.com/pub/file.zip", true),
("ftps://ftp.example.com/pub/file.zip", true),
("magnet:?xt=urn:btih:c12fe1c06bba254a9dc9", true),
("sms:+15555550123", true),
("callto:+15555550123", true),
("blob:http://localhost:5220/550e8400-e29b", true),
("mailto:test@example.com", true),
("tel:+15555550123", true),
("http://example.com/path", true),
("https://example.com/path", true),
("data:image/png;base64,iVBORw0KGgo", true),
("javascript:void(0)", true),
("file:///etc/hosts", true),
("//cdn.example.com/script.js", true),
("/abs/path", false),
("relative/path.md", false),
("./relative/path.md", false),
("../sibling/path.md", false),
("#anchor", false),
("?q=1", false),
("", false),
("C:/win/path", false),
("c:/win/path", false),
("docs/a:b.md", false),
("docs/a?x:y", false),
("12:30 notes.md", false),
("./Notes:2024.md", false),
];
for (url, expected) in cases {
assert_eq!(
is_external_url(url),
*expected,
"is_external_url({url:?}) should be {expected}"
);
}
}
}