use std::{borrow::Cow, fmt::Display, string::FromUtf8Error};
fn is_reserved(c: u8) -> bool {
c == b';'
|| c == b'/'
|| c == b'?'
|| c == b':'
|| c == b'@'
|| c == b'&'
|| c == b'='
|| c == b'+'
|| c == b'$'
|| c == b','
|| c == b'['
|| c == b']'
}
fn is_mark(c: u8) -> bool {
c == b'-'
|| c == b'_'
|| c == b'.'
|| c == b'!'
|| c == b'~'
|| c == b'*'
|| c == b'\''
|| c == b'('
|| c == b')'
}
fn is_unreserved(c: u8) -> bool {
c.is_ascii_alphanumeric() || is_mark(c)
}
fn to_hexdigit(c: u8) -> char {
assert_eq!(c & 0xF0, 0);
(if c < 10 { c + b'0' } else { c - 10 + b'A' }) as char
}
fn starts_with_pct_encoded(p: &str) -> bool {
let p = p.as_bytes();
p.len() >= 3 && p[0] == b'%' && p[1].is_ascii_hexdigit() && p[2].is_ascii_hexdigit()
}
fn starts_with_unreserved(p: &str) -> bool {
!p.is_empty() && {
let np = p.as_bytes()[0];
p.starts_with(|p: char| p.is_ascii_alphanumeric())
|| np == b'-'
|| np == b'.'
|| np == b'_'
|| np == b'~'
}
}
fn starts_with_sub_delims(p: &str) -> bool {
p.starts_with(['!', '$', '&', '(', ')', '*', '+', ',', ';', '=', '\''])
}
fn starts_with_pchar(p: &str) -> bool {
!p.is_empty() && {
let np = p.as_bytes()[0];
starts_with_unreserved(p)
|| starts_with_pct_encoded(p)
|| starts_with_sub_delims(p)
|| np == b':'
|| np == b'@'
}
}
fn starts_with_unwise(p: &str) -> bool {
p.starts_with(['{', '}', '|', '\\', '^', '[', ']', '`'])
}
const PORT_EMPTY_SERVER: Option<u16> = Some(0);
#[doc(alias = "xmlURI")]
#[derive(Debug, Default)]
pub struct XmlURI {
pub(crate) scheme: Option<Cow<'static, str>>,
pub(crate) opaque: Option<Cow<'static, str>>,
pub(crate) authority: Option<Cow<'static, str>>,
pub(crate) server: Option<Cow<'static, str>>,
pub(crate) user: Option<Cow<'static, str>>,
pub(crate) port: Option<u16>,
pub path: Option<Cow<'static, str>>,
pub(crate) query: Option<Cow<'static, str>>,
pub(crate) fragment: Option<Cow<'static, str>>,
pub(crate) cleanup: i32,
pub(crate) query_raw: Option<Cow<'static, str>>,
}
impl XmlURI {
#[doc(alias = "xmlCreateURI")]
pub fn new() -> Self {
Self::default()
}
#[doc(alias = "xmlSaveUri")]
pub fn save(&self) -> String {
let mut ret = String::new();
if let Some(scheme) = self.scheme.as_deref() {
ret.push_str(scheme);
ret.push(':');
}
if let Some(opaque) = self.opaque.as_deref() {
for p in opaque.bytes() {
if is_reserved(p) || is_unreserved(p) {
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
} else {
if self.server.is_some() || self.port.is_some() {
ret.push_str("//");
if let Some(user) = self.user.as_deref() {
for p in user.bytes() {
if is_unreserved(p)
|| p == b';'
|| p == b':'
|| p == b'&'
|| p == b'='
|| p == b'+'
|| p == b'$'
|| p == b','
{
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
ret.push('@');
}
if let Some(server) = self.server.as_deref() {
ret.push_str(server);
}
if let Some(port) = self.port.filter(|&p| p > 0) {
ret.push_str(format!(":{port}").as_str());
}
} else if let Some(authority) = self.authority.as_deref() {
ret.push_str("//");
for p in authority.bytes() {
if is_unreserved(p)
|| p == b'$'
|| p == b','
|| p == b';'
|| p == b':'
|| p == b'@'
|| p == b'&'
|| p == b'='
|| p == b'+'
{
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
}
if let Some(mut path) = self.path.as_deref() {
if self
.scheme
.as_deref()
.filter(|&scheme| {
let p = path.as_bytes();
p.len() >= 3
&& p[0] == b'/'
&& p[1].is_ascii_alphabetic()
&& p[2] == b':'
&& scheme == "file"
})
.is_some()
{
ret.push_str(&path[..3]);
path = &path[3..];
}
for p in path.bytes() {
if is_unreserved(p)
|| p == b'/'
|| p == b';'
|| p == b'@'
|| p == b'&'
|| p == b'='
|| p == b'+'
|| p == b'$'
|| p == b','
{
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
}
if let Some(query) = self.query_raw.as_deref() {
ret.push('?');
ret.push_str(query);
} else if let Some(query) = self.query.as_deref() {
ret.push('?');
for p in query.bytes() {
if is_unreserved(p) || is_reserved(p) {
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
}
}
if let Some(fragment) = self.fragment.as_deref() {
ret.push('#');
for p in fragment.bytes() {
if is_unreserved(p) || is_reserved(p) {
ret.push(p as char);
} else {
let hi = p >> 4;
let lo = p & 0xF;
ret.push('%');
ret.push(to_hexdigit(hi));
ret.push(to_hexdigit(lo));
}
}
}
ret
}
#[doc(alias = "xmlParse3986Scheme")]
fn parse3986_scheme<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
if s.starts_with(|c: char| !c.is_ascii_alphabetic()) {
return Err(2);
}
s = s.trim_start_matches(|c: char| {
c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.'
});
self.scheme = Some(orig[..orig.len() - s.len()].to_owned().into());
Ok(s)
}
#[doc(alias = "xmlParse3986Userinfo")]
fn parse3986_userinfo<'a>(&mut self, s: &'a str) -> Result<&'a str, i32> {
let mut cur = s;
while starts_with_unreserved(cur)
|| starts_with_pct_encoded(cur)
|| starts_with_sub_delims(cur)
|| cur.starts_with(':')
{
cur = if cur.starts_with('%') {
&cur[3..]
} else {
&cur[1..]
};
}
if !cur.starts_with('@') {
return Err(1);
}
self.user = if self.cleanup & 2 != 0 {
Some(s[..s.len() - cur.len()].to_owned().into())
} else {
unescape_url(&s[..s.len() - cur.len()])
.ok()
.map(|u| u.into_owned().into())
};
Ok(cur)
}
#[doc(alias = "xmlParse3986Host")]
fn parse3986_host<'a>(&mut self, s: &'a str) -> Result<&'a str, i32> {
let mut cur = s;
if let Some(rem) = cur.strip_prefix('[') {
let (_, rem) = rem.split_once(']').ok_or(1)?;
cur = rem;
} else {
fn parse_ipv4(mut s: &str) -> Result<&str, i32> {
s = parse3986_dec_octet(s)?;
for _ in 0..3 {
s = s.strip_prefix('.').ok_or(1)?;
s = parse3986_dec_octet(s)?;
}
Ok(s)
}
if let Ok(rem) = parse_ipv4(cur) {
cur = rem;
} else {
while starts_with_unreserved(cur)
|| starts_with_pct_encoded(cur)
|| starts_with_sub_delims(cur)
{
if cur.starts_with('%') {
cur = &cur[3..];
} else {
cur = &cur[1..];
}
}
}
}
self.authority = None;
self.server = None;
if cur.len() != s.len() {
self.server = if self.cleanup & 2 != 0 {
Some(s[..s.len() - cur.len()].to_owned().into())
} else {
unescape_url(&s[..s.len() - cur.len()])
.ok()
.map(|u| u.into_owned().into())
};
}
Ok(cur)
}
#[doc(alias = "xmlParse3986Port")]
fn parse3986_port<'a>(&mut self, s: &'a str) -> Result<&'a str, i32> {
if let Some((port, _)) = s
.split_once(|c: char| !c.is_ascii_digit())
.filter(|p| !p.0.is_empty())
{
self.port = Some(port.parse::<u16>().map_err(|_| 1)?);
Ok(&s[port.len()..])
} else if let Ok(port) = s.parse::<u16>() {
self.port = Some(port);
Ok("")
} else {
Err(1)
}
}
#[doc(alias = "xmlParse3986Authority")]
fn parse3986_authority<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
if let Some(rem) = self
.parse3986_userinfo(s)
.ok()
.filter(|s| s.starts_with('@'))
{
s = &rem[1..];
}
s = self.parse3986_host(s)?;
if let Some(rem) = s.strip_prefix(':') {
s = self.parse3986_port(rem)?;
}
Ok(s)
}
#[doc(alias = "xmlParse3986PathAbEmpty")]
fn parse3986_path_ab_empty<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
while let Some(rem) = s.strip_prefix('/') {
s = parse3986_segment(rem, 0, true)?;
}
self.path = None;
if s.len() != orig.len() {
if self.cleanup & 2 != 0 {
self.path = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.path = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into())
}
}
Ok(s)
}
#[doc(alias = "xmlParse3986PathAbsolute")]
fn parse3986_path_absolute<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
s = s.strip_prefix('/').ok_or(1)?;
if let Ok(rem) = parse3986_segment(s, 0, false) {
s = rem;
while let Some(rem) = s.strip_prefix('/') {
s = parse3986_segment(rem, 0, true)?;
}
}
self.path = None;
if orig.len() != s.len() {
if self.cleanup & 2 != 0 {
self.path = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.path = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into())
}
}
Ok(s)
}
#[doc(alias = "xmlParse3986PathRootless")]
fn parse3986_path_rootless<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
s = parse3986_segment(s, 0, false)?;
while let Some(rem) = s.strip_prefix('/') {
s = parse3986_segment(rem, 0, true)?;
}
self.path = None;
if orig.len() != s.len() {
if self.cleanup & 2 != 0 {
self.path = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.path = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into())
}
}
Ok(s)
}
#[doc(alias = "xmlParse3986HierPart")]
fn parse3986_hier_part<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
if let Some(rem) = s.strip_prefix("//") {
s = self.parse3986_authority(rem)?;
if self.server.is_none() && self.port.is_none() {
self.port = PORT_EMPTY_SERVER;
}
s = self.parse3986_path_ab_empty(s)?;
} else if s.starts_with('/') {
s = self.parse3986_path_absolute(s)?;
} else if starts_with_pchar(s) {
s = self.parse3986_path_rootless(s)?;
} else {
self.path = None;
}
Ok(s)
}
#[doc(alias = "xmlParse3986Query")]
fn parse3986_query<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
while starts_with_pchar(s)
|| s.starts_with(['/', '?'])
|| (self.cleanup & 1 != 0 && starts_with_unwise(s))
{
if s.starts_with('%') {
s = &s[3..];
} else {
s = &s[1..];
}
}
if self.cleanup & 2 != 0 {
self.query = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.query = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into())
}
self.query_raw = Some(orig[..orig.len() - s.len()].to_owned().into());
Ok(s)
}
#[doc(alias = "xmlParse3986Fragment")]
fn parse3986_fragment<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
while starts_with_pchar(s)
|| s.starts_with(['/', '?', '[', ']'])
|| (self.cleanup & 1 != 0 && starts_with_unwise(s))
{
if s.starts_with('%') {
s = &s[3..];
} else {
s = &s[1..];
}
}
if self.cleanup & 2 != 0 {
self.fragment = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.fragment = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into());
}
Ok(s)
}
#[doc(alias = "xmlParse3986URI")]
fn parse3986_uri<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
s = self.parse3986_scheme(s)?;
s = s.strip_prefix(':').ok_or(1)?;
s = self.parse3986_hier_part(s)?;
if let Some(rem) = s.strip_prefix('?') {
s = self.parse3986_query(rem)?;
}
if let Some(rem) = s.strip_prefix('#') {
s = self.parse3986_fragment(rem)?;
}
if !s.is_empty() {
*self = Self::default();
Err(1)
} else {
Ok(s)
}
}
#[doc(alias = "xmlParse3986PathNoScheme")]
fn parse3986_path_no_scheme<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
let orig = s;
s = parse3986_segment(s, b':', false)?;
while let Some(rem) = s.strip_prefix('/') {
s = parse3986_segment(rem, 0, true)?;
}
self.path = None;
if orig.len() != s.len() {
if self.cleanup & 2 != 0 {
self.path = Some(orig[..orig.len() - s.len()].to_owned().into());
} else {
self.path = unescape_url(&orig[..orig.len() - s.len()])
.ok()
.map(|u| u.into_owned().into())
}
}
Ok(s)
}
#[doc(alias = "xmlParse3986RelativeRef")]
fn parse3986_relative_ref<'a>(&mut self, mut s: &'a str) -> Result<&'a str, i32> {
if let Some(rem) = s.strip_prefix("//") {
s = self.parse3986_authority(rem)?;
s = self.parse3986_path_ab_empty(s)?;
} else if s.starts_with('/') {
s = self.parse3986_path_absolute(s)?;
} else if starts_with_pchar(s) {
s = self.parse3986_path_no_scheme(s)?;
} else {
self.path = None;
}
if let Some(rem) = s.strip_prefix('?') {
s = self.parse3986_query(rem)?;
}
if let Some(rem) = s.strip_prefix('#') {
s = self.parse3986_fragment(rem)?;
}
if !s.is_empty() {
*self = Self::default();
Err(1)
} else {
Ok(s)
}
}
#[doc(alias = "xmlParse3986URIReference")]
fn parse3986_uri_reference(&mut self, s: &str) -> Result<(), i32> {
*self = Self::default();
if self.parse3986_uri(s).is_err() {
*self = Self::default();
if let Err(code) = self.parse3986_relative_ref(s) {
*self = Self::default();
return Err(code);
}
}
Ok(())
}
#[doc(alias = "xmlParseURIReference")]
pub fn parse_uri_reference(&mut self, s: &str) -> Result<(), i32> {
self.parse3986_uri_reference(s)
}
#[doc(alias = "xmlParseURI", alias = "xmlParseURIRaw")]
pub fn parse(s: &str) -> Option<Self> {
let mut uri = Self::new();
uri.parse3986_uri_reference(s).ok()?;
Some(uri)
}
}
impl Display for XmlURI {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.save())
}
}
#[doc(alias = "xmlParse3986DecOctet")]
fn parse3986_dec_octet(s: &str) -> Result<&str, i32> {
let mut t = s[..3.min(s.len())].split_terminator(|c: char| !c.is_ascii_digit());
let t = t.next().filter(|t| !t.is_empty()).ok_or(1)?;
let u = t.parse::<u16>().unwrap();
if u < 256 { Ok(&s[t.len()..]) } else { Err(1) }
}
#[doc(alias = "xmlParse3986Segment")]
fn parse3986_segment(mut s: &str, forbid: u8, empty: bool) -> Result<&str, i32> {
if !starts_with_pchar(s) {
if empty {
return Ok(s);
}
return Err(1);
}
while starts_with_pchar(s) && !s.as_bytes().starts_with(&[forbid]) {
if s.starts_with('%') {
s = &s[3..];
} else {
s = &s[1..];
}
}
Ok(s)
}
#[doc(alias = "xmlBuildURI")]
pub fn build_uri(uri: &str, base: &str) -> Option<String> {
let mut val = None;
let mut refe = None;
if !uri.is_empty() {
let mut r = XmlURI::new();
r.parse_uri_reference(uri).ok()?;
refe = Some(r);
}
if refe.as_ref().is_some_and(|r| r.scheme.is_some()) {
return Some(uri.to_owned());
}
let mut bas = XmlURI::new();
if bas.parse_uri_reference(base).is_err() {
if let Some(refe) = refe.as_ref() {
val = Some(refe.save());
}
return val;
}
let Some(refe) = refe else {
bas.fragment = None;
return Some(bas.save());
};
let mut res = XmlURI::new();
'step_7: {
if refe.scheme.is_none()
&& refe.path.is_none()
&& (refe.authority.is_none() && refe.server.is_none() && refe.port.is_none())
{
res.scheme = bas.scheme.clone();
if bas.authority.is_some() {
res.authority = bas.authority.clone();
} else {
res.server = bas.server.clone();
res.user = bas.user.clone();
res.port = bas.port;
}
res.path = bas.path.clone();
if refe.query_raw.is_some() {
res.query_raw = refe.query_raw.clone();
} else if refe.query.is_some() {
res.query = refe.query.clone();
} else if bas.query_raw.is_some() {
res.query_raw = bas.query_raw.clone();
} else if bas.query.is_some() {
res.query = bas.query.clone();
}
res.fragment = refe.fragment.clone();
break 'step_7;
}
if refe.scheme.is_some() {
return Some(refe.save());
}
if bas.scheme.is_some() {
res.scheme = bas.scheme.clone();
}
if refe.query_raw.is_some() {
res.query_raw = refe.query_raw.clone();
} else if refe.query.is_some() {
res.query = refe.query.clone();
}
if refe.fragment.is_some() {
res.fragment = refe.fragment.clone();
}
if refe.authority.is_some() || refe.server.is_some() || refe.port.is_some() {
if refe.authority.is_some() {
res.authority = refe.authority.clone();
} else {
if refe.server.is_some() {
res.server = refe.server.clone();
}
if refe.user.is_some() {
res.user = refe.user.clone();
}
res.port = refe.port;
}
if refe.path.is_some() {
res.path = refe.path.clone();
}
break 'step_7;
}
if bas.authority.is_some() {
res.authority = bas.authority.clone();
} else if bas.server.is_some() || bas.port.is_some() {
if bas.server.is_some() {
res.server = bas.server.clone();
}
if bas.user.is_some() {
res.user = bas.user.clone();
}
res.port = bas.port;
}
if let Some(path) = refe.path.as_deref().filter(|p| p.starts_with('/')) {
res.path = Some(path.to_owned().into());
break 'step_7;
}
let mut path = String::new();
if let Some(bas_path) = bas.path.as_deref() {
if let Some(pos) = bas_path.rfind('/') {
path.push_str(&bas_path[..pos + 1]);
}
}
if let Some(ref_path) = refe.path.as_deref().filter(|p| !p.is_empty()) {
if path.is_empty() && (bas.server.is_some() || bas.port.is_some()) {
path.push('/');
}
path.push_str(ref_path);
}
match normalize_uri_path(&path) {
Cow::Owned(path) => res.path = Some(Cow::Owned(path)),
Cow::Borrowed(_) => res.path = Some(Cow::Owned(path)),
}
}
Some(res.save())
}
#[doc(alias = "xmlBuildRelativeURI")]
pub fn build_relative_uri<'a>(uri: &'a str, base: Option<&str>) -> Option<Cow<'a, str>> {
if uri.is_empty() {
return None;
}
let mut refe = XmlURI::new();
if !uri.starts_with('.') {
refe.parse_uri_reference(uri).ok()?;
} else {
refe.path = Some(uri.to_owned().into());
}
let Some(base) = base.filter(|base| !base.is_empty()) else {
return Some(uri.into());
};
let mut bas = XmlURI::new();
if !base.starts_with('.') {
bas.parse_uri_reference(base).ok()?;
} else {
bas.path = Some(base.to_owned().into());
}
if let Some(rscheme) = refe.scheme.as_deref() {
if bas.scheme.is_none_or(|scheme| scheme != rscheme)
|| bas.port != refe.port
|| bas.server != refe.server
{
return Some(uri.into());
}
}
if bas.path == refe.path {
return Some("".into());
}
let Some(base_path) = bas.path.as_deref() else {
return refe.path;
};
let refe_path = refe.path.unwrap_or_else(|| "/".into());
let mut bptr = base_path;
let mut rptr = refe_path.as_ref();
if let Some(rem) = rptr.strip_prefix("./") {
rptr = rem;
}
if let Some(rem) = bptr.strip_prefix("./") {
bptr = rem;
} else if let Some(rem) = bptr.strip_prefix('/').filter(|_| !rptr.starts_with('/')) {
bptr = rem;
}
let pos = bptr
.bytes()
.zip(rptr.bytes())
.take_while(|(b, r)| r == b)
.count();
if bptr.len() == rptr.len() && pos == bptr.len() {
return Some("".into());
}
let ix = rptr.as_bytes()[..pos]
.iter()
.rposition(|&b| b == b'/')
.map(|pos| pos + 1)
.unwrap_or(0);
let uptr = &rptr[ix..];
let nbslash = bptr.as_bytes()[ix..].iter().filter(|&&b| b == b'/').count();
if nbslash == 0 && uptr.is_empty() {
return Some("./".into());
}
if nbslash == 0 {
return Some(escape_url_except(uptr, b"/;&=+$,").into_owned().into());
}
let mut res = String::with_capacity(uptr.len() + 3 * nbslash);
for _ in 0..nbslash {
res.push_str("../");
}
if !res.is_empty() && !uptr.is_empty() && uptr.starts_with('/') && res.ends_with('/') {
res.push_str(&uptr[1..]);
} else {
res.push_str(uptr);
}
Some(escape_url_except(&res, b"/;&=+$,").into_owned().into())
}
#[doc(alias = "xmlURIEscapeStr")]
pub fn escape_url_except<'a>(s: &'a str, except: &[u8]) -> Cow<'a, str> {
if s.is_empty() {
return Cow::Borrowed(s);
}
let mut ret = String::with_capacity(s.len());
for ch in s.bytes() {
if ch != b'@' && !is_unreserved(ch) && !except.contains(&ch) {
ret.push('%');
let hi = to_hexdigit(ch >> 4);
let lo = to_hexdigit(ch & 0x0F);
ret.push(hi);
ret.push(lo);
} else {
ret.push(ch as char);
}
}
Cow::Owned(ret)
}
#[doc(alias = "xmlURIEscape")]
pub fn escape_url(s: &str) -> Option<String> {
use std::fmt::Write as _;
let mut uri = XmlURI::new();
uri.cleanup = 1;
uri.parse_uri_reference(s).ok()?;
let mut ret = String::new();
if let Some(scheme) = uri.scheme.as_deref() {
let segment = escape_url_except(scheme, b"+-.");
write!(ret, "{segment}:").ok()?;
}
if let Some(authority) = uri.authority.as_deref() {
let segment = escape_url_except(authority, b"/?;:@");
write!(ret, "//{segment}").ok()?;
}
if let Some(user) = uri.user.as_deref() {
let segment = escape_url_except(user, b";:&=+$,");
write!(ret, "//{segment}@").ok()?;
}
if let Some(server) = uri.server.as_deref() {
let segment = escape_url_except(server, b"/?;:@");
if uri.user.is_none() {
write!(ret, "//").ok()?;
}
write!(ret, "{segment}").ok()?;
}
if let Some(port) = uri.port.filter(|&p| p > 0) {
write!(ret, ":{port}").ok()?;
}
if let Some(path) = uri.path.as_deref() {
let segment = escape_url_except(path, b":@&=+$,/?;");
write!(ret, "{segment}").ok()?;
}
if let Some(query) = uri.query_raw.as_deref() {
write!(ret, "?{query}").ok()?;
} else if let Some(query) = uri.query.as_deref() {
let segment = escape_url_except(query, b";/?:@&=+,$");
write!(ret, "?{segment}").ok()?;
}
if let Some(opaque) = uri.opaque.as_deref() {
let segment = escape_url_except(opaque, b"");
write!(ret, "{segment}").ok()?;
}
if let Some(fragment) = uri.fragment.as_deref() {
let segment = escape_url_except(fragment, b"#");
write!(ret, "#{segment}").ok()?;
}
Some(ret)
}
#[doc(alias = "xmlURIUnescapeString")]
pub fn unescape_url(mut url: &str) -> Result<Cow<'_, str>, FromUtf8Error> {
let mut consumed = 0;
let mut keep = 0;
let mut owned = None;
let hex2dec = |hex: u8| -> u8 {
match hex {
b'0'..=b'9' => hex - b'0',
b'a'..=b'f' => hex - b'a' + 10,
b'A'..=b'F' => hex - b'A' + 10,
_ => unreachable!(),
}
};
let orig = url;
while let Some((non_escaped, maybe_escaped)) = url.split_once('%') {
keep += non_escaped.len();
match maybe_escaped.as_bytes() {
&[hi, lo, ..] if hi.is_ascii_hexdigit() && lo.is_ascii_hexdigit() => {
let owned = owned.get_or_insert_with(|| Vec::with_capacity(orig.len()));
owned.extend_from_slice(orig[consumed..keep].as_bytes());
let (hi, lo) = (hex2dec(hi), hex2dec(lo));
owned.push((hi << 4) | lo);
url = &maybe_escaped[2..];
keep += 3;
consumed = keep;
}
_ => {
keep += 1;
url = maybe_escaped;
}
}
}
if let Some(mut owned) = owned {
owned.extend_from_slice(url.as_bytes());
String::from_utf8(owned).map(Cow::<str>::Owned)
} else {
Ok(Cow::Borrowed(url))
}
}
#[doc(alias = "xmlNormalizeURIPath")]
pub fn normalize_uri_path(path: &str) -> Cow<'_, str> {
let Some(pos) = path.find(|c: char| c != '/') else {
return Cow::Borrowed(path);
};
let (head, path) = path.split_at(pos);
let mut splited = path.split('/');
let last = splited.next_back().unwrap();
let mut segments = vec![];
for seg in splited.filter(|&seg| seg != "." && !seg.is_empty()) {
if seg == ".." {
if segments.is_empty() || segments.last().copied() == Some("..") {
segments.push(seg);
} else {
segments.pop();
}
} else {
segments.push(seg);
}
}
if last == ".." && segments.last().copied() != Some("..") {
segments.pop();
segments.push("");
} else if last != "." {
segments.push(last);
} else if last == "." {
segments.push("");
}
let mut path = head.to_owned();
if !segments.is_empty() {
path.push_str(segments[0]);
for seg in segments.into_iter().skip(1) {
path.push('/');
path.push_str(seg);
}
}
Cow::Owned(path)
}
#[doc(alias = "xmlCanonicPath")]
pub fn canonic_path(mut path: &str) -> Cow<'_, str> {
#[cfg(target_os = "windows")]
let mut len: i32 = 0;
#[cfg(target_os = "windows")]
let mut p: *mut c_char = null_mut();
#[cfg(target_os = "windows")]
{
if path.starts_with("\\\\?\\") {
return Cow::Borrowed(path);
}
}
if path.starts_with("//") && (path.len() < 3 || path.as_bytes()[2] != b'/') {
path = &path[1..];
}
if XmlURI::parse(path).is_some() {
return Cow::Borrowed(path);
};
if let Some(pos) = path.find("://") {
if pos <= 20 {
if path[..pos].bytes().all(|c| c.is_ascii_alphabetic()) {
let esc_uri = escape_url_except(path, b":/?_.#&;=");
if XmlURI::parse(esc_uri.as_ref()).is_some() {
return esc_uri;
}
}
}
}
#[cfg(target_os = "windows")]
{
let uri = XmlURI::new();
if len > 2 && IS_WINDOWS_PATH!(path) {
uri.scheme = Some(Cow::Borrowed("file"));
uri.path = Some(Cow::Owned(format!("/{}", path.replace('\\', '/'))));
} else {
uri.path = Some(Cow::Owned(path.replace('\\', '/')));
}
if uri.scheme.is_some() {
Cow::Owned(uri.save())
} else {
uri.path.unwrap()
}
}
#[cfg(not(target_os = "windows"))]
{
Cow::Borrowed(path)
}
}
#[doc(alias = "xmlPathToURI")]
pub fn path_to_uri(path: &str) -> Cow<'_, str> {
if XmlURI::parse(path).is_some() {
return Cow::Borrowed(path);
}
#[cfg(not(target_os = "windows"))]
let cal = canonic_path(path);
#[cfg(target_os = "windows")]
let mut cal = canonic_path(path);
#[cfg(target_os = "windows")]
{
if XmlURI::parse(&cal).is_some() {
return cal;
}
let ret = cal.replace('\\', '/');
cal = Cow::Owned(ret);
}
let temp = XmlURI {
path: Some(cal.into_owned().into()),
..Default::default()
};
Cow::Owned(temp.save())
}