use std::ptr::null_mut;
use std::slice;
use nyquest_interface::Error as NyquestError;
use windows_sys::Win32::Networking::WinHttp::*;
use windows_sys::Win32::UI::Shell::*;
pub(crate) struct ParsedUrl<'b> {
pub host: &'b [u16],
pub port: u16,
pub path_and_query: &'b [u16],
pub is_secure: bool,
}
impl<'b> ParsedUrl<'b> {
pub fn parse(url_wide: &'b [u16]) -> Option<Self> {
let mut components;
let result;
unsafe {
components = URL_COMPONENTS {
dwStructSize: std::mem::size_of::<URL_COMPONENTS>() as u32,
lpszScheme: null_mut(),
dwSchemeLength: u32::MAX,
nScheme: 0,
lpszHostName: null_mut(),
dwHostNameLength: u32::MAX,
nPort: 0,
lpszUserName: null_mut(),
dwUserNameLength: 0,
lpszPassword: null_mut(),
dwPasswordLength: 0,
lpszUrlPath: null_mut(),
dwUrlPathLength: u32::MAX,
lpszExtraInfo: null_mut(),
dwExtraInfoLength: 0,
};
result = WinHttpCrackUrl(
url_wide.as_ptr(),
u32::try_from(url_wide.len()).unwrap_or(u32::MAX),
0,
&mut components,
)
}
if result == 0 {
return None;
}
let is_secure = components.nScheme == 2;
let port = if components.nPort == 0 {
if is_secure {
443
} else {
80
}
} else {
components.nPort
};
unsafe {
let host = slice::from_raw_parts(
components.lpszHostName,
components.dwHostNameLength as usize,
);
let path = if components.dwUrlPathLength == 0 {
&[b'/' as u16]
} else {
slice::from_raw_parts(components.lpszUrlPath, components.dwUrlPathLength as usize)
};
Some(ParsedUrl {
host,
port,
path_and_query: path,
is_secure,
})
}
}
}
pub(crate) fn concat_url(
base_url_wide: Option<&[u16]>,
relative_uri: &str,
) -> Result<Vec<u16>, NyquestError> {
let mut relative_wide: Vec<u16> = relative_uri
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let is_absolute = relative_uri.starts_with("http://") || relative_uri.starts_with("https://");
if let Some(base_wide) = base_url_wide.filter(|_| !is_absolute) {
if !base_wide.ends_with(&[0]) {
panic!("base_url must be null-terminated wide string");
}
let estimated_len = base_wide.len() * 2 + relative_wide.len() + 2;
let mut buffer_len = u32::try_from(estimated_len).map_err(|_| NyquestError::InvalidUrl)?;
let mut buffer = vec![0u16; estimated_len];
let result = unsafe {
UrlCombineW(
base_wide.as_ptr(),
relative_wide.as_ptr(),
buffer.as_mut_ptr(),
&mut buffer_len,
0, )
};
return if result == 0 {
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
buffer.truncate(len);
buffer.shrink_to_fit();
Ok(buffer)
} else {
Err(NyquestError::InvalidUrl)
};
}
relative_wide.pop();
Ok(relative_wide)
}
#[cfg(test)]
mod tests {
use super::*;
use widestring::{u16cstr, u16str};
#[test]
fn test_parse_http_url() {
let url = ParsedUrl::parse(u16cstr!("http://example.com/path").as_slice()).unwrap();
assert_eq!(url.host, u16cstr!("example.com").as_slice());
assert_eq!(url.port, 80);
assert_eq!(url.path_and_query, u16cstr!("/path").as_slice());
assert!(!url.is_secure);
}
#[test]
fn test_parse_https_url_with_port() {
let url = ParsedUrl::parse(u16cstr!("https://example.com:8443/api/v1").as_slice()).unwrap();
assert_eq!(url.host, u16cstr!("example.com").as_slice());
assert_eq!(url.port, 8443);
assert_eq!(url.path_and_query, u16cstr!("/api/v1").as_slice());
assert!(url.is_secure);
}
#[test]
fn test_parse_url_no_path() {
let url = ParsedUrl::parse(u16cstr!("https://example.com?1").as_slice()).unwrap();
assert_eq!(url.host, u16cstr!("example.com").as_slice());
assert_eq!(url.port, 443);
assert_eq!(url.path_and_query, u16cstr!("?1").as_slice());
assert!(url.is_secure);
}
#[test]
fn test_concat_url() {
assert_eq!(
concat_url(
Some(u16cstr!("https://api.example.com").as_slice_with_nul()),
"/users"
)
.unwrap(),
u16str!("https://api.example.com/users").as_slice()
);
assert_eq!(
concat_url(
Some(u16cstr!("https://api.example.com/aa").as_slice_with_nul()),
"users?id=1"
)
.unwrap(),
u16str!("https://api.example.com/users?id=1").as_slice()
);
assert_eq!(
concat_url(None, "https://example.com/path").unwrap(),
u16str!("https://example.com/path").as_slice()
);
}
}