use percent_encoding::percent_decode_str;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default)]
enum State {
#[default]
Command,
Query,
Done,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct UrlArgs {
url: String,
next_arg: usize,
state: State,
}
impl UrlArgs {
#[must_use]
pub fn new() -> Self {
#[cfg(not(all(target_arch = "wasm32", feature = "urlargs")))]
{
"".into()
}
#[cfg(all(target_arch = "wasm32", feature = "urlargs"))]
{
match web_sys::window() {
Some(window) => match window.location().href() {
Ok(href) => href,
Err(_) => "Unable to access `Location`".into(),
},
None => "Unable to access `Window`".into(),
}
.into()
}
}
}
impl Iterator for UrlArgs {
type Item = std::ffi::OsString;
fn next(&mut self) -> Option<Self::Item> {
let begin = self.next_arg;
let delim = match self.state {
State::Command => Some('?'),
State::Query => Some('&'),
State::Done => None,
};
match delim {
None => None,
Some(delim) => {
let arg = match self.url[begin..].find(|c| (c == delim) | (c == '#')) {
None => {
self.state = State::Done;
self.next_arg = self.url.len();
&self.url[begin..]
}
Some(index) => {
let end = begin + index;
self.next_arg = end + 1;
let arg = &self.url[begin..=end];
if let Some(arg) = arg.strip_suffix(delim) {
self.state = State::Query;
arg
} else if let Some(arg) = arg.strip_suffix('#') {
self.state = State::Done;
arg
} else {
unreachable!("Arg did not end in delim or #");
}
}
};
let arg: String = percent_decode_str(arg).decode_utf8_lossy().into();
Some(arg.into())
}
}
}
}
impl From<&str> for UrlArgs {
fn from(url: &str) -> Self {
String::from(url).into()
}
}
impl From<String> for UrlArgs {
fn from(url: String) -> Self {
Self {
url,
next_arg: 0,
state: State::default(),
}
}
}
impl Default for UrlArgs {
fn default() -> Self {
"".into()
}
}
#[cfg(test)]
mod tests {
use crate::url_args::UrlArgs;
use std::ffi::OsString;
fn do_parse(url: &str, expected_args: Vec<OsString>) {
let args: UrlArgs = url.into();
let args: Vec<_> = args.collect();
assert_eq!(args, expected_args);
}
#[test]
fn with_hash() {
let url = "http:///www.example.org/index.html#hash";
let expected_args = vec!["http:///www.example.org/index.html".into()];
do_parse(url, expected_args);
}
#[test]
fn simple_query() {
let url = "http:///www.example.org/index.html?--first&second&third#hash";
let expected_args = vec![
"http:///www.example.org/index.html".into(),
"--first".into(),
"second".into(),
"third".into(),
];
do_parse(url, expected_args);
}
#[test]
fn percent_encoded() {
let url = "http:///www.exam%20ple.org/index.html?foo%20%3Cbar%3E";
let expected_args = vec![
"http:///www.exam ple.org/index.html".into(),
"foo <bar>".into(),
];
do_parse(url, expected_args);
}
#[test]
fn strange_chars() {
let url = "(http\"///www&example&org*index\\html?first&second&third)#hash";
let expected_args = vec![
"(http\"///www&example&org*index\\html".into(),
"first".into(),
"second".into(),
"third)".into(),
];
do_parse(url, expected_args);
}
#[test]
fn invalid() {
let url = "http:///www.example.org/index.html?foo%f0%3Cbar%3E&second&third#hash";
let expected_args = vec![
"http:///www.example.org/index.html".into(),
"foo\u{FFFD}<bar>".into(),
"second".into(),
"third".into(),
];
do_parse(url, expected_args);
}
#[test]
fn nulls() {
let url = "?&&#";
let expected_args = vec![OsString::new(), OsString::new(), OsString::new(), "".into()];
do_parse(url, expected_args);
let url = "#?&&";
let expected_args = vec![OsString::from("")];
do_parse(url, expected_args);
let url = "";
let expected_args = vec![OsString::from("")];
do_parse(url, expected_args);
}
}