use encoding_rs::Encoding;
use thiserror::Error;
use crate::util::Utf16String;
pub struct Uris;
impl Uris {
pub const fn new() -> Self {
Self
}
pub fn escape_path(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, None, UriComponent::Path)
}
pub fn escape_path_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, encoding, UriComponent::Path)
}
pub fn unescape_path(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, None)
}
pub fn unescape_path_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, encoding)
}
pub fn escape_path_segment(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, None, UriComponent::PathSegment)
}
pub fn escape_path_segment_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, encoding, UriComponent::PathSegment)
}
pub fn unescape_path_segment(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, None)
}
pub fn unescape_path_segment_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, encoding)
}
pub fn escape_fragment_id(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, None, UriComponent::Fragment)
}
pub fn escape_fragment_id_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, encoding, UriComponent::Fragment)
}
pub fn unescape_fragment_id(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, None)
}
pub fn unescape_fragment_id_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, encoding)
}
pub fn escape_query_param(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, None, UriComponent::QueryParameter)
}
pub fn escape_query_param_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
escape(text, encoding, UriComponent::QueryParameter)
}
pub fn unescape_query_param(
&self,
text: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, None)
}
pub fn unescape_query_param_with_encoding(
&self,
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
unescape(text, encoding)
}
}
impl Default for Uris {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy)]
enum UriComponent {
Path,
PathSegment,
Fragment,
QueryParameter,
}
fn escape(
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
component: UriComponent,
) -> Result<Option<Utf16String>, UriExpressionError> {
let Some(text) = text else {
return Ok(None);
};
let encoding = resolve_encoding(encoding)?;
let source = text.to_string_lossy();
let (bytes, _, _) = encoding.encode(&source);
let mut output = String::with_capacity(bytes.len());
for byte in bytes.iter().copied() {
if byte.is_ascii() && is_allowed(byte, component) {
output.push(char::from(byte));
} else {
output.push('%');
output.push(hex(byte >> 4));
output.push(hex(byte & 0x0F));
}
}
Ok(Some(Utf16String::from_rust_str(&output)))
}
fn unescape(
text: Option<&Utf16String>,
encoding: Option<&Utf16String>,
) -> Result<Option<Utf16String>, UriExpressionError> {
let Some(text) = text else {
return Ok(None);
};
let encoding = resolve_encoding(encoding)?;
let source = text.to_string_lossy();
if !source.as_bytes().contains(&b'%') {
return Ok(Some(text.clone()));
}
let input = source.as_bytes();
let mut output = String::with_capacity(input.len());
let mut position = 0;
while position < input.len() {
if input[position] != b'%' {
let character = source[position..]
.chars()
.next()
.expect("position remains on UTF-8 boundary");
output.push(character);
position += character.len_utf8();
continue;
}
let mut bytes = Vec::new();
while position + 2 < input.len() && input[position] == b'%' {
let Some(high) = from_hex(input[position + 1]) else {
break;
};
let Some(low) = from_hex(input[position + 2]) else {
break;
};
bytes.push((high << 4) | low);
position += 3;
}
if bytes.is_empty() {
output.push('%');
position += 1;
} else {
let (decoded, _, _) = encoding.decode(&bytes);
output.push_str(&decoded);
}
}
Ok(Some(Utf16String::from_rust_str(&output)))
}
fn resolve_encoding(
encoding: Option<&Utf16String>,
) -> Result<&'static Encoding, UriExpressionError> {
let Some(encoding) = encoding else {
return Ok(encoding_rs::UTF_8);
};
Encoding::for_label(encoding.to_string_lossy().as_bytes()).ok_or_else(|| {
UriExpressionError::UnsupportedEncoding {
encoding: encoding.to_string_lossy(),
}
})
}
fn is_allowed(byte: u8, component: UriComponent) -> bool {
if byte.is_ascii_alphanumeric() || b"-._~!$'()*,:;@".contains(&byte) {
return true;
}
match component {
UriComponent::Path => matches!(byte, b'&' | b'+' | b'=' | b'/'),
UriComponent::PathSegment => matches!(byte, b'&' | b'+' | b'='),
UriComponent::Fragment => matches!(byte, b'&' | b'+' | b'=' | b'/' | b'?'),
UriComponent::QueryParameter => matches!(byte, b'/' | b'?'),
}
}
fn hex(value: u8) -> char {
char::from(if value < 10 {
b'0' + value
} else {
b'A' + value - 10
})
}
fn from_hex(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
b'A'..=b'F' => Some(value - b'A' + 10),
_ => None,
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum UriExpressionError {
#[error("Unsupported encoding: {encoding}")]
UnsupportedEncoding {
encoding: String,
},
}