#![allow(clippy::missing_safety_doc)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
use crate::abi::allocator;
use crate::abi::types::*;
use core::ffi::c_void;
use core::ptr;
use std::os::raw::{c_char, c_int};
fn is_unreserved(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-' || b == b'.' || b == b'_' || b == b'~'
}
fn is_reserved(b: u8) -> bool {
is_gen_delim(b) || is_sub_delim(b)
}
fn is_scheme_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.'
}
fn is_gen_delim(b: u8) -> bool {
matches!(b, b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@')
}
fn is_sub_delim(b: u8) -> bool {
matches!(
b,
b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
)
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
fn percent_decode(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
if data[i] == b'%' && i + 2 < data.len() {
if let Some(h) = hex_val(data[i + 1]) {
if let Some(l) = hex_val(data[i + 2]) {
result.push((h << 4) | l);
i += 3;
continue;
}
}
}
result.push(data[i]);
i += 1;
}
result
}
fn percent_encode(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
for &b in data {
if is_unreserved(b) || is_reserved(b) || b == b'%' {
result.push(b);
} else {
result.extend_from_slice(format!("%{:02X}", b).as_bytes());
}
}
result
}
#[derive(Debug, Clone, Default)]
pub(crate) struct UriParts {
pub scheme: Option<Vec<u8>>, pub opaque: Option<Vec<u8>>, pub authority: Option<Vec<u8>>, pub server: Option<Vec<u8>>, pub user: Option<Vec<u8>>, pub host: Option<Vec<u8>>, pub port: c_int, pub path: Option<Vec<u8>>, pub query: Option<Vec<u8>>, pub fragment: Option<Vec<u8>>, pub path_raw: Option<Vec<u8>>, pub clean_path: Option<Vec<u8>>, }
fn find_scheme(uri: &[u8]) -> Option<(usize, usize)> {
if uri.is_empty() {
return None;
}
if !uri[0].is_ascii_alphabetic() {
return None;
}
let mut i = 1;
while i < uri.len() && is_scheme_char(uri[i]) {
i += 1;
}
if i < uri.len() && uri[i] == b':' {
Some((0, i))
} else {
None
}
}
fn parse_authority(auth: &[u8]) -> (Option<Vec<u8>>, Option<Vec<u8>>, c_int) {
let mut user: Option<Vec<u8>> = None;
let mut host: Option<Vec<u8>> = None;
let mut port: c_int = 0;
if auth.is_empty() {
return (None, None, 0);
}
let (user_part, host_part) = if let Some(at_pos) = auth.iter().position(|&b| b == b'@') {
user = Some(auth[..at_pos].to_vec());
(&auth[at_pos + 1..], true)
} else {
(auth, false)
};
if user_part.starts_with(b"[") {
if let Some(close_bracket) = user_part.iter().position(|&b| b == b']') {
let host_end = close_bracket + 1;
host = Some(user_part[..host_end].to_vec());
if host_end < user_part.len() && user_part[host_end] == b':' {
let port_str = &user_part[host_end + 1..];
if !port_str.is_empty() {
let port_str_decoded = core::str::from_utf8(port_str).unwrap_or("");
port = port_str_decoded.parse::<c_int>().unwrap_or(0);
}
}
} else {
host = Some(user_part.to_vec());
}
} else {
if let Some(colon_pos) = user_part.iter().position(|&b| b == b':') {
host = Some(user_part[..colon_pos].to_vec());
let port_str = &user_part[colon_pos + 1..];
if !port_str.is_empty() {
let port_str_decoded = core::str::from_utf8(port_str).unwrap_or("");
port = port_str_decoded.parse::<c_int>().unwrap_or(0);
}
} else {
host = Some(user_part.to_vec());
}
}
(user, host, port)
}
pub(crate) fn parse_uri(str: &[u8]) -> Option<UriParts> {
if str.is_empty() {
return None;
}
let mut parts = UriParts::default();
let mut remaining = str;
if let Some((_start, end)) = find_scheme(remaining) {
parts.scheme = Some(remaining[..end].to_vec());
remaining = &remaining[end + 1..];
if remaining.starts_with(b"//") {
remaining = &remaining[2..];
let auth_end = remaining
.iter()
.position(|&b| b == b'/' || b == b'?' || b == b'#')
.unwrap_or(remaining.len());
let authority = &remaining[..auth_end];
parts.authority = if authority.is_empty() {
Some(Vec::new())
} else {
Some(authority.to_vec())
};
if !authority.is_empty() {
let (user, host, port) = parse_authority(authority);
parts.user = user;
parts.host = host;
parts.port = port;
if let Some(ref host_val) = parts.host {
let mut server = host_val.clone();
if port != 0 {
server.extend_from_slice(format!(":{}", port).as_bytes());
}
parts.server = Some(server);
}
}
remaining = &remaining[auth_end..];
} else {
if let Some(frag_pos) = remaining.iter().position(|&b| b == b'#') {
parts.opaque = Some(remaining[..frag_pos].to_vec());
parts.fragment = Some(remaining[frag_pos + 1..].to_vec());
} else {
parts.opaque = Some(remaining.to_vec());
}
parts.path = parts.opaque.clone();
return Some(parts);
}
}
let query_pos = remaining.iter().position(|&b| b == b'?');
let frag_pos = remaining.iter().position(|&b| b == b'#');
let path_end = match (query_pos, frag_pos) {
(Some(q), Some(f)) => q.min(f),
(Some(q), None) => q,
(None, Some(f)) => f,
(None, None) => remaining.len(),
};
if path_end > 0 {
let path = remaining[..path_end].to_vec();
parts.path = Some(path.clone());
parts.path_raw = Some(path);
}
if let Some(qpos) = query_pos {
let qstart = qpos + 1;
let qend = frag_pos.unwrap_or(remaining.len());
if qstart < qend {
parts.query = Some(remaining[qstart..qend].to_vec());
}
}
if let Some(fpos) = frag_pos {
let fstart = fpos + 1;
if fstart < remaining.len() {
parts.fragment = Some(remaining[fstart..].to_vec());
}
}
Some(parts)
}
pub(crate) fn parse_uri_cstr(str: *const xmlChar) -> *mut UriParts {
if str.is_null() {
return ptr::null_mut();
}
let len = unsafe { libc::strlen(str as *const libc::c_char) };
let slice = unsafe { core::slice::from_raw_parts(str, len) };
match parse_uri(slice) {
Some(parts) => {
let boxed = Box::new(parts);
Box::into_raw(boxed)
}
None => ptr::null_mut(),
}
}
pub(crate) unsafe fn free_uri_parts(parts: *mut UriParts) {
if !parts.is_null() {
drop(Box::from_raw(parts));
}
}
pub(crate) fn build_uri(parts: &UriParts) -> Vec<u8> {
let mut result = Vec::new();
if let Some(ref scheme) = parts.scheme {
result.extend_from_slice(scheme);
result.push(b':');
}
if let Some(ref authority) = parts.authority {
result.extend_from_slice(b"//");
result.extend_from_slice(authority);
} else if parts.host.is_some() {
result.extend_from_slice(b"//");
if let Some(ref user) = parts.user {
result.extend_from_slice(user);
result.push(b'@');
}
if let Some(ref host) = parts.host {
result.extend_from_slice(host);
}
if parts.port != 0 {
result.push(b':');
result.extend_from_slice(format!("{}", parts.port).as_bytes());
}
}
if let Some(ref path) = parts.path {
result.extend_from_slice(path);
} else if let Some(ref opaque) = parts.opaque {
result.extend_from_slice(opaque);
}
if let Some(ref query) = parts.query {
result.push(b'?');
result.extend_from_slice(query);
}
if let Some(ref fragment) = parts.fragment {
result.push(b'#');
result.extend_from_slice(fragment);
}
result
}
pub(crate) fn normalize_uri_path(uri: &[u8]) -> Vec<u8> {
if uri.is_empty() {
return Vec::new();
}
let absolute = uri.starts_with(b"/");
let ends_with_slash = uri.ends_with(b"/");
let parts: Vec<&[u8]> = uri.split(|&b| b == b'/').collect();
let mut segments: Vec<&[u8]> = Vec::new();
for segment in parts {
if segment == b"." || segment.is_empty() {
continue;
}
if segment == b".." {
segments.pop();
} else {
segments.push(segment);
}
}
let mut result = Vec::new();
if absolute {
result.push(b'/');
}
for (i, seg) in segments.iter().enumerate() {
if i > 0 {
result.push(b'/');
}
result.extend_from_slice(seg);
}
if ends_with_slash && !segments.is_empty() {
result.push(b'/');
}
if result.is_empty() && absolute {
result.push(b'/');
}
result
}
pub(crate) fn get_scheme(uri: &[u8]) -> Option<Vec<u8>> {
if let Some((_start, end)) = find_scheme(uri) {
Some(uri[_start..end].to_vec())
} else {
None
}
}
pub(crate) fn is_absolute(uri: &[u8]) -> bool {
find_scheme(uri).is_some()
}
pub(crate) fn resolve_uri(base: &[u8], relative: &[u8]) -> Option<Vec<u8>> {
if base.is_empty() {
return if relative.is_empty() {
None
} else {
Some(relative.to_vec())
};
}
if is_absolute(relative) {
return Some(relative.to_vec());
}
let base_parts = parse_uri(base)?;
if relative.is_empty() {
return Some(build_uri(&base_parts));
}
let rel_str = relative;
let mut result = UriParts {
scheme: base_parts.scheme.clone(),
..Default::default()
};
if rel_str.starts_with(b"//") {
let rest = &rel_str[2..];
let auth_end = rest.iter().position(|&b| b == b'/').unwrap_or(rest.len());
let auth = rest[..auth_end].to_vec();
let (user, host, port) = parse_authority(&auth);
result.authority = Some(auth);
result.user = user;
result.host = host;
result.port = port;
let path_rest = if auth_end < rest.len() {
&rest[auth_end..]
} else {
b""
};
parse_path_query_fragment(path_rest, &mut result);
} else if rel_str.starts_with(b"/") {
parse_path_query_fragment(rel_str, &mut result);
result.authority = base_parts.authority.clone();
result.user = base_parts.user.clone();
result.host = base_parts.host.clone();
result.port = base_parts.port;
} else {
let base_path = base_parts.path.as_deref().unwrap_or(b"");
let base_dir = if let Some(last_slash) = base_path.iter().rposition(|&b| b == b'/') {
&base_path[..=last_slash]
} else {
b""
};
let mut combined = Vec::from(base_dir);
combined.extend_from_slice(rel_str);
parse_path_query_fragment(&combined, &mut result);
result.authority = base_parts.authority.clone();
result.user = base_parts.user.clone();
result.host = base_parts.host.clone();
result.port = base_parts.port;
}
if let Some(ref path) = result.path {
let normalized = normalize_uri_path(path);
result.path = Some(normalized);
}
Some(build_uri(&result))
}
fn parse_path_query_fragment(input: &[u8], parts: &mut UriParts) {
let query_pos = input.iter().position(|&b| b == b'?');
let frag_pos = input.iter().position(|&b| b == b'#');
let path_end = match (query_pos, frag_pos) {
(Some(q), Some(f)) => q.min(f),
(Some(q), None) => q,
(None, Some(f)) => f,
(None, None) => input.len(),
};
if path_end > 0 {
parts.path = Some(input[..path_end].to_vec());
parts.path_raw = parts.path.clone();
}
if let Some(qpos) = query_pos {
let qstart = qpos + 1;
let qend = frag_pos.unwrap_or(input.len());
if qstart < qend {
parts.query = Some(input[qstart..qend].to_vec());
}
}
if let Some(fpos) = frag_pos {
let fstart = fpos + 1;
if fstart < input.len() {
parts.fragment = Some(input[fstart..].to_vec());
}
}
}
pub(crate) unsafe fn xmlParseURI(str: *const c_char) -> *mut c_void {
if str.is_null() {
return ptr::null_mut();
}
let len = libc::strlen(str);
let slice = unsafe { core::slice::from_raw_parts(str as *const u8, len) };
match parse_uri(slice) {
Some(parts) => {
let boxed = Box::new(parts);
Box::into_raw(boxed) as *mut c_void
}
None => ptr::null_mut(),
}
}
pub(crate) unsafe fn xmlFreeURI(uri: *mut c_void) {
if !uri.is_null() {
drop(Box::from_raw(uri as *mut UriParts));
}
}
pub(crate) fn xmlCreateURI() -> *mut c_void {
let parts = UriParts::default();
let boxed = Box::new(parts);
Box::into_raw(boxed) as *mut c_void
}
pub(crate) unsafe fn xmlSaveUri(uri: *mut c_void) -> *mut xmlChar {
if uri.is_null() {
return ptr::null_mut();
}
let parts = unsafe { &*(uri as *const UriParts) };
let result = build_uri(parts);
if result.is_empty() {
return ptr::null_mut();
}
let len = result.len();
let ptr = unsafe { allocator::xmlMalloc(len + 1) as *mut u8 };
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
ptr::copy_nonoverlapping(result.as_ptr(), ptr, len);
*ptr.add(len) = 0; }
ptr as *mut xmlChar
}
pub(crate) unsafe fn xmlURIEscapeStr(str: *const xmlChar, list: *const xmlChar) -> *mut xmlChar {
if str.is_null() {
return ptr::null_mut();
}
let str_len = unsafe { libc::strlen(str as *const libc::c_char) };
let str_slice = unsafe { core::slice::from_raw_parts(str, str_len) };
let mut safe_set = [false; 256];
for b in 0u8..=255 {
if is_unreserved(b) || b == b'%' {
safe_set[b as usize] = true;
}
}
if !list.is_null() {
let list_len = unsafe { libc::strlen(list as *const libc::c_char) };
let list_slice = unsafe { core::slice::from_raw_parts(list, list_len) };
for &b in list_slice {
safe_set[b as usize] = true;
}
}
let mut result = Vec::with_capacity(str_slice.len() * 3);
for &b in str_slice {
if safe_set[b as usize] {
result.push(b);
} else {
result.extend_from_slice(format!("%{:02X}", b).as_bytes());
}
}
let len = result.len();
let ptr = unsafe { allocator::xmlMalloc(len + 1) as *mut u8 };
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
ptr::copy_nonoverlapping(result.as_ptr(), ptr, len);
*ptr.add(len) = 0;
}
ptr as *mut xmlChar
}
pub(crate) unsafe fn xmlURIUnescapeString(
str: *const c_char,
len: c_int,
target: *mut c_char,
) -> *mut c_char {
if str.is_null() {
return ptr::null_mut();
}
let slice = if len < 0 {
let cstr_len = unsafe { libc::strlen(str) };
unsafe { core::slice::from_raw_parts(str as *const u8, cstr_len) }
} else {
unsafe { core::slice::from_raw_parts(str as *const u8, len as usize) }
};
let decoded = percent_decode(slice);
if !target.is_null() {
unsafe {
ptr::copy_nonoverlapping(decoded.as_ptr(), target as *mut u8, decoded.len());
*((target as *mut u8).add(decoded.len())) = 0;
}
return target;
}
let out_len = decoded.len();
let ptr = unsafe { allocator::xmlMalloc(out_len + 1) as *mut u8 };
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
ptr::copy_nonoverlapping(decoded.as_ptr(), ptr, out_len);
*ptr.add(out_len) = 0;
}
ptr as *mut c_char
}
pub(crate) unsafe fn xmlParseURIRaw(str: *const c_char, _raw: c_int) -> *mut c_void {
unsafe { xmlParseURI(str) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_unreserved() {
assert!(is_unreserved(b'a'));
assert!(is_unreserved(b'Z'));
assert!(is_unreserved(b'0'));
assert!(is_unreserved(b'-'));
assert!(is_unreserved(b'.'));
assert!(is_unreserved(b'_'));
assert!(is_unreserved(b'~'));
assert!(!is_unreserved(b':'));
assert!(!is_unreserved(b'/'));
assert!(!is_unreserved(b'%'));
assert!(!is_unreserved(b' '));
}
#[test]
fn test_is_reserved() {
assert!(is_reserved(b':'));
assert!(is_reserved(b'/'));
assert!(is_reserved(b'?'));
assert!(is_reserved(b'#'));
assert!(is_reserved(b'@'));
assert!(is_reserved(b'!'));
assert!(is_reserved(b'$'));
assert!(is_reserved(b'&'));
assert!(is_reserved(b'('));
assert!(is_reserved(b')'));
assert!(!is_reserved(b'a'));
assert!(!is_reserved(b' '));
}
#[test]
fn test_is_scheme_char() {
assert!(is_scheme_char(b'a'));
assert!(is_scheme_char(b'Z'));
assert!(is_scheme_char(b'0'));
assert!(is_scheme_char(b'+'));
assert!(is_scheme_char(b'-'));
assert!(is_scheme_char(b'.'));
assert!(!is_scheme_char(b':'));
assert!(!is_scheme_char(b'/'));
assert!(!is_scheme_char(b' '));
}
#[test]
fn test_percent_decode_simple() {
assert_eq!(percent_decode(b"hello"), b"hello");
assert_eq!(percent_decode(b"%68%65%6C%6C%6F"), b"hello");
assert_eq!(percent_decode(b"%48%65%6C%6C%6F"), b"Hello");
assert_eq!(percent_decode(b"a%20b"), b"a b");
}
#[test]
fn test_percent_decode_invalid() {
assert_eq!(percent_decode(b"%XX"), b"%XX");
assert_eq!(percent_decode(b"%2"), b"%2");
assert_eq!(percent_decode(b"%"), b"%");
assert_eq!(percent_decode(b"%%20"), b"% ");
}
#[test]
fn test_percent_decode_empty() {
assert_eq!(percent_decode(b""), b"");
}
#[test]
fn test_percent_encode() {
assert_eq!(percent_encode(b"hello"), b"hello");
assert_eq!(percent_encode(b"hello world"), b"hello%20world");
assert_eq!(percent_encode(b"a/b"), b"a/b"); }
#[test]
fn test_parse_http_uri() {
let parts =
parse_uri(b"http://example.com/path/to/file.xml?query=1#frag").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.authority, Some(b"example.com".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
assert_eq!(parts.port, 0);
assert_eq!(parts.path, Some(b"/path/to/file.xml".to_vec()));
assert_eq!(parts.query, Some(b"query=1".to_vec()));
assert_eq!(parts.fragment, Some(b"frag".to_vec()));
}
#[test]
fn test_parse_https_uri() {
let parts = parse_uri(b"https://example.com:443/path").expect("should parse");
assert_eq!(parts.scheme, Some(b"https".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
assert_eq!(parts.port, 443);
assert_eq!(parts.path, Some(b"/path".to_vec()));
}
#[test]
fn test_parse_file_uri() {
let parts = parse_uri(b"file:///etc/hosts").expect("should parse");
assert_eq!(parts.scheme, Some(b"file".to_vec()));
assert!(parts.authority.is_none() || parts.authority.as_deref() == Some(b""));
assert_eq!(parts.path, Some(b"/etc/hosts".to_vec()));
}
#[test]
fn test_parse_file_uri_with_host() {
let parts = parse_uri(b"file://localhost/etc/hosts").expect("should parse");
assert_eq!(parts.scheme, Some(b"file".to_vec()));
assert_eq!(parts.host, Some(b"localhost".to_vec()));
assert_eq!(parts.path, Some(b"/etc/hosts".to_vec()));
}
#[test]
fn test_parse_relative_uri() {
let parts = parse_uri(b"/path/to/file.xml").expect("should parse");
assert!(parts.scheme.is_none());
assert_eq!(parts.path, Some(b"/path/to/file.xml".to_vec()));
}
#[test]
fn test_parse_relative_uri_with_query() {
let parts = parse_uri(b"file.xml?query=1").expect("should parse");
assert!(parts.scheme.is_none());
assert_eq!(parts.path, Some(b"file.xml".to_vec()));
assert_eq!(parts.query, Some(b"query=1".to_vec()));
}
#[test]
fn test_parse_uri_with_user_info() {
let parts = parse_uri(b"ftp://user@host.com:21/path").expect("should parse");
assert_eq!(parts.scheme, Some(b"ftp".to_vec()));
assert_eq!(parts.user, Some(b"user".to_vec()));
assert_eq!(parts.host, Some(b"host.com".to_vec()));
assert_eq!(parts.port, 21);
assert_eq!(parts.path, Some(b"/path".to_vec()));
}
#[test]
fn test_parse_uri_with_user_password() {
let parts = parse_uri(b"ftp://user:pass@host.com/path").expect("should parse");
assert_eq!(parts.scheme, Some(b"ftp".to_vec()));
assert_eq!(parts.user, Some(b"user:pass".to_vec()));
assert_eq!(parts.host, Some(b"host.com".to_vec()));
assert_eq!(parts.path, Some(b"/path".to_vec()));
}
#[test]
fn test_parse_opaque_uri() {
let parts = parse_uri(b"mailto:user@example.com").expect("should parse");
assert_eq!(parts.scheme, Some(b"mailto".to_vec()));
assert_eq!(parts.opaque, Some(b"user@example.com".to_vec()));
assert!(parts.authority.is_none());
}
#[test]
fn test_parse_opaque_uri_with_fragment() {
let parts = parse_uri(b"urn:isbn:0-395-36341-1#frag").expect("should parse");
assert_eq!(parts.scheme, Some(b"urn".to_vec()));
assert_eq!(parts.opaque, Some(b"isbn:0-395-36341-1".to_vec()));
assert_eq!(parts.fragment, Some(b"frag".to_vec()));
}
#[test]
fn test_parse_empty_uri() {
assert!(parse_uri(b"").is_none());
}
#[test]
fn test_parse_uri_fragment_only() {
let parts = parse_uri(b"#fragment").expect("should parse");
assert!(parts.scheme.is_none());
assert!(parts.path.is_none());
assert_eq!(parts.fragment, Some(b"fragment".to_vec()));
}
#[test]
fn test_parse_uri_query_only() {
let parts = parse_uri(b"?query").expect("should parse");
assert!(parts.scheme.is_none());
assert!(parts.path.is_none());
assert_eq!(parts.query, Some(b"query".to_vec()));
}
#[test]
fn test_parse_uri_with_ipv6_host() {
let parts = parse_uri(b"http://[::1]:8080/path").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.host, Some(b"[::1]".to_vec()));
assert_eq!(parts.port, 8080);
assert_eq!(parts.path, Some(b"/path".to_vec()));
}
#[test]
fn test_parse_uri_no_path() {
let parts = parse_uri(b"http://example.com").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
assert!(parts.path.is_none());
}
#[test]
fn test_parse_uri_no_path_with_query() {
let parts = parse_uri(b"http://example.com?query").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
assert!(parts.path.is_none());
assert_eq!(parts.query, Some(b"query".to_vec()));
}
#[test]
fn test_build_uri() {
let parts = UriParts {
scheme: Some(b"http".to_vec()),
host: Some(b"example.com".to_vec()),
port: 8080,
path: Some(b"/path".to_vec()),
query: Some(b"q=1".to_vec()),
fragment: Some(b"frag".to_vec()),
..Default::default()
};
assert_eq!(build_uri(&parts), b"http://example.com:8080/path?q=1#frag");
}
#[test]
fn test_build_uri_simple() {
let parts = UriParts {
scheme: Some(b"http".to_vec()),
host: Some(b"example.com".to_vec()),
path: Some(b"/".to_vec()),
..Default::default()
};
assert_eq!(build_uri(&parts), b"http://example.com/");
}
#[test]
fn test_build_uri_opaque() {
let parts = UriParts {
scheme: Some(b"mailto".to_vec()),
opaque: Some(b"user@example.com".to_vec()),
..Default::default()
};
assert_eq!(build_uri(&parts), b"mailto:user@example.com");
}
#[test]
fn test_build_uri_relative() {
let parts = UriParts {
path: Some(b"/relative/path".to_vec()),
..Default::default()
};
assert_eq!(build_uri(&parts), b"/relative/path");
}
#[test]
fn test_normalize_uri_path_simple() {
assert_eq!(normalize_uri_path(b"/foo/bar"), b"/foo/bar");
assert_eq!(normalize_uri_path(b"/foo/./bar"), b"/foo/bar");
assert_eq!(normalize_uri_path(b"/foo/../bar"), b"/bar");
assert_eq!(normalize_uri_path(b"/foo/bar/.."), b"/foo");
assert_eq!(normalize_uri_path(b"/"), b"/");
}
#[test]
fn test_normalize_uri_path_relative() {
assert_eq!(normalize_uri_path(b"foo/bar"), b"foo/bar");
assert_eq!(normalize_uri_path(b"foo/./bar"), b"foo/bar");
assert_eq!(normalize_uri_path(b"foo/../bar"), b"bar");
}
#[test]
fn test_normalize_uri_path_double_dot_overflow() {
assert_eq!(normalize_uri_path(b"/a/../../b"), b"/b");
assert_eq!(normalize_uri_path(b"/../b"), b"/b");
}
#[test]
fn test_normalize_uri_path_empty() {
assert_eq!(normalize_uri_path(b""), b"");
}
#[test]
fn test_normalize_uri_path_dots_only() {
assert_eq!(normalize_uri_path(b"./././."), b"");
assert_eq!(normalize_uri_path(b"/./././"), b"/");
}
#[test]
fn test_get_scheme() {
assert_eq!(get_scheme(b"http://example.com"), Some(b"http".to_vec()));
assert_eq!(get_scheme(b"https://example.com"), Some(b"https".to_vec()));
assert_eq!(get_scheme(b"file:///path"), Some(b"file".to_vec()));
assert_eq!(get_scheme(b"ftp://host"), Some(b"ftp".to_vec()));
assert_eq!(get_scheme(b"mailto:user@host"), Some(b"mailto".to_vec()));
assert_eq!(get_scheme(b"urn:isbn:1234"), Some(b"urn".to_vec()));
assert_eq!(get_scheme(b"/path"), None);
assert_eq!(get_scheme(b"relative"), None);
assert_eq!(get_scheme(b""), None);
}
#[test]
fn test_is_absolute() {
assert!(is_absolute(b"http://example.com"));
assert!(is_absolute(b"file:///path"));
assert!(is_absolute(b"mailto:user@host"));
assert!(!is_absolute(b"/path"));
assert!(!is_absolute(b"relative"));
assert!(!is_absolute(b""));
}
#[test]
fn test_resolve_uri_absolute_relative() {
let result =
resolve_uri(b"http://example.com/base/", b"relative.xml").expect("should resolve");
assert_eq!(result, b"http://example.com/base/relative.xml");
}
#[test]
fn test_resolve_uri_absolute_absolute() {
let result = resolve_uri(
b"http://example.com/base/",
b"http://other.com/absolute.xml",
)
.expect("should resolve");
assert_eq!(result, b"http://other.com/absolute.xml");
}
#[test]
fn test_resolve_uri_root_relative() {
let result =
resolve_uri(b"http://example.com/base/file.xml", b"/root.xml").expect("should resolve");
assert_eq!(result, b"http://example.com/root.xml");
}
#[test]
fn test_resolve_uri_network_path() {
let result = resolve_uri(b"http://example.com/base/file.xml", b"//other.com/root.xml")
.expect("should resolve");
assert_eq!(result, b"http://other.com/root.xml");
}
#[test]
fn test_resolve_uri_parent_traversal() {
let result = resolve_uri(b"http://example.com/a/b/c/file.xml", b"../../d/file.xml")
.expect("should resolve");
assert_eq!(result, b"http://example.com/a/d/file.xml");
}
#[test]
fn test_resolve_uri_with_query() {
let result =
resolve_uri(b"http://example.com/base/", b"file.xml?query=1").expect("should resolve");
assert_eq!(result, b"http://example.com/base/file.xml?query=1");
}
#[test]
fn test_resolve_uri_with_fragment() {
let result =
resolve_uri(b"http://example.com/base/file.xml", b"#frag").expect("should resolve");
assert_eq!(result, b"http://example.com/base/#frag");
}
#[test]
fn test_resolve_uri_empty_base() {
let result = resolve_uri(b"", b"relative.xml");
assert_eq!(result, Some(b"relative.xml".to_vec()));
}
#[test]
fn test_resolve_uri_empty_relative() {
let result = resolve_uri(b"http://example.com/base/", b"");
assert!(result.is_some());
assert_eq!(result.unwrap(), b"http://example.com/base/");
}
#[test]
fn test_resolve_uri_both_empty() {
assert!(resolve_uri(b"", b"").is_none());
}
#[test]
fn test_resolve_uri_file_scheme() {
let result = resolve_uri(b"file:///base/dir/", b"file.xml").expect("should resolve");
assert_eq!(result, b"file:///base/dir/file.xml");
}
#[test]
fn test_resolve_uri_deep_relative() {
let result = resolve_uri(
b"http://example.com/a/b/c/d/e/file.xml",
b"../../../../x/y/z/file.xml",
)
.expect("should resolve");
assert_eq!(result, b"http://example.com/a/x/y/z/file.xml");
}
#[test]
fn test_xml_create_and_free_uri() {
unsafe {
let uri = xmlCreateURI();
assert!(!uri.is_null());
xmlFreeURI(uri);
}
}
#[test]
fn test_xml_parse_uri() {
unsafe {
let cstr = b"http://example.com/path\0".as_ptr() as *const c_char;
let uri = xmlParseURI(cstr);
assert!(!uri.is_null());
let parts = &*(uri as *const UriParts);
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
xmlFreeURI(uri);
}
}
#[test]
fn test_xml_parse_uri_null() {
unsafe {
let uri = xmlParseURI(ptr::null());
assert!(uri.is_null());
}
}
#[test]
fn test_xml_save_uri() {
unsafe {
let cstr = b"http://example.com:8080/path?q=1#f\0".as_ptr() as *const c_char;
let uri = xmlParseURI(cstr);
assert!(!uri.is_null());
let saved = xmlSaveUri(uri);
assert!(!saved.is_null());
let saved_str = std::ffi::CStr::from_ptr(saved as *const c_char);
assert_eq!(saved_str.to_bytes(), b"http://example.com:8080/path?q=1#f");
allocator::xmlFree(saved as *mut core::ffi::c_void);
xmlFreeURI(uri);
}
}
#[test]
fn test_xml_escape_str() {
unsafe {
let cstr = b"hello world\0".as_ptr() as *const xmlChar;
let result = xmlURIEscapeStr(cstr, ptr::null());
assert!(!result.is_null());
let result_str = std::ffi::CStr::from_ptr(result as *const c_char);
assert_eq!(result_str.to_bytes(), b"hello%20world");
allocator::xmlFree(result as *mut core::ffi::c_void);
}
}
#[test]
fn test_xml_escape_str_with_safe_list() {
unsafe {
let cstr = b"hello world\0".as_ptr() as *const xmlChar;
let safe = b" \0".as_ptr() as *const xmlChar;
let result = xmlURIEscapeStr(cstr, safe);
assert!(!result.is_null());
let result_str = std::ffi::CStr::from_ptr(result as *const c_char);
assert_eq!(result_str.to_bytes(), b"hello world"); allocator::xmlFree(result as *mut core::ffi::c_void);
}
}
#[test]
fn test_xml_unescape_string() {
unsafe {
let cstr = b"hello%20world\0".as_ptr() as *const c_char;
let result = xmlURIUnescapeString(cstr, -1, ptr::null_mut());
assert!(!result.is_null());
let result_str = std::ffi::CStr::from_ptr(result);
assert_eq!(result_str.to_bytes(), b"hello world");
allocator::xmlFree(result as *mut core::ffi::c_void);
}
}
#[test]
fn test_xml_unescape_string_with_len() {
unsafe {
let cstr = b"hello%20world\0".as_ptr() as *const c_char;
let result = xmlURIUnescapeString(cstr, 13, ptr::null_mut());
assert!(!result.is_null());
let result_str = std::ffi::CStr::from_ptr(result);
assert_eq!(result_str.to_bytes(), b"hello world");
allocator::xmlFree(result as *mut core::ffi::c_void);
}
}
#[test]
fn test_xml_parse_uri_raw() {
unsafe {
let cstr = b"http://example.com\0".as_ptr() as *const c_char;
let uri = xmlParseURIRaw(cstr, 0);
assert!(!uri.is_null());
let parts = &*(uri as *const UriParts);
assert_eq!(parts.scheme, Some(b"http".to_vec()));
xmlFreeURI(uri);
}
}
#[test]
fn test_xml_free_null() {
unsafe {
xmlFreeURI(ptr::null_mut());
}
}
#[test]
fn test_parse_uri_scheme_only() {
let parts = parse_uri(b"http:").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert!(parts.opaque.is_none() || parts.opaque.as_deref() == Some(b""));
}
#[test]
fn test_parse_uri_with_trailing_slash() {
let parts = parse_uri(b"http://example.com/").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.path, Some(b"/".to_vec()));
}
#[test]
fn test_parse_uri_with_double_slash_path() {
let parts = parse_uri(b"http://example.com//path").expect("should parse");
assert_eq!(parts.scheme, Some(b"http".to_vec()));
assert_eq!(parts.path, Some(b"//path".to_vec()));
}
#[test]
fn test_parse_uri_no_scheme_colon() {
let parts = parse_uri(b"123:path");
assert!(parts.is_some());
let p = parts.unwrap();
assert!(p.scheme.is_none());
assert_eq!(p.path, Some(b"123:path".to_vec()));
}
#[test]
fn test_parse_uri_ftp_with_home_dir() {
let parts = parse_uri(b"ftp://host/home/user/file.txt").expect("should parse");
assert_eq!(parts.scheme, Some(b"ftp".to_vec()));
assert_eq!(parts.host, Some(b"host".to_vec()));
assert_eq!(parts.path, Some(b"/home/user/file.txt".to_vec()));
}
#[test]
fn test_parse_uri_scheme_case() {
let parts = parse_uri(b"HTTP://example.com/Path").expect("should parse");
assert_eq!(parts.scheme, Some(b"HTTP".to_vec()));
assert_eq!(parts.host, Some(b"example.com".to_vec()));
assert_eq!(parts.path, Some(b"/Path".to_vec()));
}
#[test]
fn test_normalize_path_complex() {
assert_eq!(normalize_uri_path(b"/a/b/c/./../../g"), b"/a/g");
assert_eq!(normalize_uri_path(b"mid/content=5/../6"), b"mid/6");
}
#[test]
fn test_resolve_uri_same_directory() {
let result =
resolve_uri(b"http://example.com/a/b/c.html", b"d.html").expect("should resolve");
assert_eq!(result, b"http://example.com/a/b/d.html");
}
#[test]
fn test_resolve_uri_complex_traversal() {
let result = resolve_uri(b"http://a/b/c/d;p?q", b"g/h/../i/./j#f").expect("should resolve");
let result_str = core::str::from_utf8(&result).unwrap_or("");
assert!(result_str.contains("http://a/b/c/g/i/j"));
}
#[test]
fn test_hex_val() {
assert_eq!(hex_val(b'0'), Some(0));
assert_eq!(hex_val(b'9'), Some(9));
assert_eq!(hex_val(b'a'), Some(10));
assert_eq!(hex_val(b'f'), Some(15));
assert_eq!(hex_val(b'A'), Some(10));
assert_eq!(hex_val(b'F'), Some(15));
assert_eq!(hex_val(b'g'), None);
assert_eq!(hex_val(b'z'), None);
assert_eq!(hex_val(b'%'), None);
}
#[test]
fn test_parse_uri_cstr() {
let cstr = b"http://example.com/path\0".as_ptr() as *const xmlChar;
let ptr = parse_uri_cstr(cstr);
assert!(!ptr.is_null());
unsafe {
assert_eq!((*ptr).scheme, Some(b"http".to_vec()));
assert_eq!((*ptr).host, Some(b"example.com".to_vec()));
free_uri_parts(ptr);
}
}
#[test]
fn test_parse_uri_cstr_null() {
let ptr = parse_uri_cstr(ptr::null());
assert!(ptr.is_null());
}
#[test]
fn test_parse_uri_cstr_invalid() {
let cstr = b"\0".as_ptr() as *const xmlChar;
let ptr = parse_uri_cstr(cstr);
assert!(ptr.is_null());
}
}