1use std::path::{Path, PathBuf};
6
7#[non_exhaustive]
9#[derive(Debug, thiserror::Error)]
10pub enum UriError {
11 #[error("unsupported URI scheme: {0}")]
13 UnsupportedScheme(String),
14 #[error("URI is not valid UTF-8 after percent decoding")]
16 InvalidUtf8,
17 #[error("path cannot be expressed as a file URI: {0}")]
19 InvalidPath(PathBuf),
20}
21
22pub fn uri_to_path(uri: &str) -> Result<PathBuf, UriError> {
27 let rest = uri
28 .strip_prefix("file://")
29 .ok_or_else(|| UriError::UnsupportedScheme(uri.to_owned()))?;
30 let trimmed = rest.strip_prefix('/').unwrap_or(rest);
31 let decoded = percent_decode(trimmed)?;
32 if cfg!(windows) {
33 let normalized = decoded.replace('/', "\\");
34 Ok(PathBuf::from(normalized))
35 } else {
36 Ok(PathBuf::from(format!("/{decoded}")))
37 }
38}
39
40pub fn path_to_uri(path: &Path) -> Result<String, UriError> {
46 if !path.is_absolute() {
47 return Err(UriError::InvalidPath(path.to_path_buf()));
48 }
49 let as_str = path
50 .to_str()
51 .ok_or_else(|| UriError::InvalidPath(path.to_path_buf()))?;
52 let normalized = as_str.replace('\\', "/");
53 let encoded = percent_encode(&normalized);
54 if normalized.starts_with('/') {
55 Ok(format!("file://{encoded}"))
56 } else {
57 Ok(format!("file:///{encoded}"))
58 }
59}
60
61fn percent_decode(input: &str) -> Result<String, UriError> {
62 let bytes = input.as_bytes();
63 let mut out = Vec::with_capacity(bytes.len());
64 let mut idx = 0;
65 while idx < bytes.len() {
66 if bytes[idx] == b'%' && idx + 2 < bytes.len() {
67 let hi = hex_digit(bytes[idx + 1])?;
68 let lo = hex_digit(bytes[idx + 2])?;
69 out.push((hi << 4) | lo);
70 idx += 3;
71 } else {
72 out.push(bytes[idx]);
73 idx += 1;
74 }
75 }
76 String::from_utf8(out).map_err(|_| UriError::InvalidUtf8)
77}
78
79fn hex_digit(byte: u8) -> Result<u8, UriError> {
80 match byte {
81 b'0'..=b'9' => Ok(byte - b'0'),
82 b'a'..=b'f' => Ok(byte - b'a' + 10),
83 b'A'..=b'F' => Ok(byte - b'A' + 10),
84 _ => Err(UriError::InvalidUtf8),
85 }
86}
87
88fn percent_encode(input: &str) -> String {
89 use std::fmt::Write as _;
90 let mut out = String::with_capacity(input.len());
91 for byte in input.bytes() {
92 match byte {
93 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' | b':' => {
94 out.push(byte as char);
95 }
96 _ => {
97 let _ = write!(out, "%{byte:02X}");
98 }
99 }
100 }
101 out
102}