Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ tao = { version = "0.33.0", features = ["rwh_05"] }
webbrowser = "1.0.3"
infer = "0.19.0"
dunce = "1.0.5"
urlencoding = "2.1.3"
percent-encoding = "2.3.1"
global-hotkey = "0.6.4"
rfd = { version = "0.15.2", default-features = false }
muda = "0.16.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/asset-resolver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ rust-version = "1.79.0"

[dependencies]
http = { workspace = true }
urlencoding = { workspace = true }
percent-encoding = { workspace = true }
infer = { workspace = true }
thiserror = { workspace = true }
dioxus-cli-config = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion packages/asset-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetS
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
// The path is the first part of the URI, so we need to trim the leading slash.
let mut uri_path = PathBuf::from(
urlencoding::decode(path)
percent_encoding::percent_decode_str(path)
.decode_utf8()
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ slab = { workspace = true }
rustc-hash = { workspace = true }
dioxus-hooks = { workspace = true }
futures-util = { workspace = true }
urlencoding = { workspace = true }
percent-encoding = { workspace = true }
async-trait = { workspace = true }
tao = { workspace = true, features = ["rwh_05"] }
dioxus-history = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion packages/router-macro/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl HashFragment {
{
let __hash = #ident.to_string();
if !__hash.is_empty() {
write!(f, "#{}", __hash)?;
write!(f, "#{}", dioxus_router::exports::percent_encoding::utf8_percent_encode(&__hash, dioxus_router::exports::FRAGMENT_ASCII_SET))?;
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions packages/router-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,17 @@ impl RouteEnum {
// Remove any trailing slashes. We parse /route/ and /route in the same way
// Note: we don't use trim because it includes more code
let route = route.strip_suffix('/').unwrap_or(route);
let query = dioxus_router::exports::urlencoding::decode(query).unwrap_or(query.into());
let hash = dioxus_router::exports::urlencoding::decode(hash).unwrap_or(hash.into());
let mut segments = route.split('/').map(|s| dioxus_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
let query = dioxus_router::exports::percent_encoding::percent_decode_str(query)
.decode_utf8()
.unwrap_or(query.into());
let hash = dioxus_router::exports::percent_encoding::percent_decode_str(hash)
.decode_utf8()
.unwrap_or(hash.into());
let mut segments = route.split('/').map(|s| {
dioxus_router::exports::percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap_or(s.into())
});
// skip the first empty segment
if s.starts_with('/') {
let _ = segments.next();
Expand Down
28 changes: 16 additions & 12 deletions packages/router-macro/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,8 @@ impl QuerySegment {
QuerySegment::Segments(segments) => {
let mut tokens = TokenStream2::new();
tokens.extend(quote! { write!(f, "?")?; });
let mut segments_iter = segments.iter();
if let Some(first_segment) = segments_iter.next() {
tokens.extend(first_segment.write());
}
for segment in segments_iter {
tokens.extend(quote! { write!(f, "&")?; });
tokens.extend(segment.write());
for (i, segment) in segments.iter().enumerate() {
tokens.extend(segment.write(i == segments.len() - 1));
}
tokens
}
Expand Down Expand Up @@ -133,7 +128,7 @@ impl FullQuerySegment {
quote! {
{
let as_string = #ident.to_string();
write!(f, "?{}", dioxus_router::exports::urlencoding::encode(&as_string))?;
write!(f, "?{}", dioxus_router::exports::percent_encoding::utf8_percent_encode(&as_string, dioxus_router::exports::QUERY_ASCII_SET))?;
}
}
}
Expand All @@ -151,18 +146,27 @@ impl QueryArgument {
let ty = &self.ty;
quote! {
let #ident = match split_query.get(stringify!(#ident)) {
Some(query_argument) => <#ty as dioxus_router::routable::FromQueryArgument>::from_query_argument(query_argument).unwrap_or_default(),
Some(query_argument) => {
use dioxus_router::routable::FromQueryArgument;
<#ty>::from_query_argument(query_argument).unwrap_or_default()
},
None => <#ty as Default>::default(),
};
}
}

pub fn write(&self) -> TokenStream2 {
pub fn write(&self, trailing: bool) -> TokenStream2 {
let ident = &self.ident;
let write_ampersand = if !trailing {
quote! { if !as_string.is_empty() { write!(f, "&")?; } }
} else {
quote! {}
};
quote! {
{
let as_string = #ident.to_string();
write!(f, "{}={}", stringify!(#ident), dioxus_router::exports::urlencoding::encode(&as_string))?;
let as_string = dioxus_router::routable::DisplayQueryArgument::new(stringify!(#ident), #ident).to_string();
write!(f, "{}", dioxus_router::exports::percent_encoding::utf8_percent_encode(&as_string, dioxus_router::exports::QUERY_ASCII_SET))?;
#write_ampersand
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/router-macro/src/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl RouteSegment {
Self::Dynamic(ident, _) => quote! {
{
let as_string = #ident.to_string();
write!(f, "/{}", dioxus_router::exports::urlencoding::encode(&as_string))?;
write!(f, "/{}", dioxus_router::exports::percent_encoding::utf8_percent_encode(&as_string, dioxus_router::exports::PATH_ASCII_SET))?;
}
},
Self::CatchAll(ident, _) => quote! { #ident.display_route_segments(f)?; },
Expand Down
2 changes: 1 addition & 1 deletion packages/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dioxus-history = { workspace = true }
dioxus-router-macro = { workspace = true }
dioxus-fullstack-hooks = { workspace = true, optional = true }
tracing = { workspace = true }
urlencoding = { workspace = true }
percent-encoding = { workspace = true }
url = { workspace = true }
dioxus-cli-config = { workspace = true }
rustversion = { workspace = true }
Expand Down
33 changes: 32 additions & 1 deletion packages/router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,36 @@ mod utils {

#[doc(hidden)]
pub mod exports {
pub use urlencoding;
pub use crate::query_sets::*;
pub use percent_encoding;
}

pub(crate) mod query_sets {
//! Url percent encode sets defined [here](https://url.spec.whatwg.org/#percent-encoded-bytes)

use percent_encoding::AsciiSet;

/// The ASCII set that must be escaped in query strings.
pub const QUERY_ASCII_SET: &AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'<')
.add(b'>');

/// The ASCII set that must be escaped in path segments.
pub const PATH_ASCII_SET: &AsciiSet = &QUERY_ASCII_SET
.add(b'?')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'}');

/// The ASCII set that must be escaped in hash fragments.
pub const FRAGMENT_ASCII_SET: &AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
}
139 changes: 136 additions & 3 deletions packages/router/src/routable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
/// }
/// }
///
/// // We also need to implement Display for CustomQuery which will be used to format the query string into the URL
/// // We also need to implement Display for CustomQuery so that ToQueryArgument is implemented automatically
/// impl std::fmt::Display for CustomQuery {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}", self.count)
Expand All @@ -143,7 +143,7 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
note = "FromQueryArgument is automatically implemented for types that implement `FromStr` and `Default`. You need to either implement FromStr and Default or implement FromQueryArgument manually."
)
)]
pub trait FromQueryArgument: Default {
pub trait FromQueryArgument<P = ()>: Default {
/// The error that can occur when parsing a query argument.
type Err;

Expand All @@ -168,6 +168,138 @@ where
}
}

/// A marker type for `Option<T>` to implement `FromQueryArgument`.
pub struct OptionMarker;

impl<T: Default + FromStr> FromQueryArgument<OptionMarker> for Option<T>
where
<T as FromStr>::Err: Display,
{
type Err = <T as FromStr>::Err;

fn from_query_argument(argument: &str) -> Result<Self, Self::Err> {
match T::from_str(argument) {
Ok(result) => Ok(Some(result)),
Err(err) => {
tracing::error!("Failed to parse query argument: {}", err);
Err(err)
}
}
}
}

/// Something that can be formatted as a query argument. This trait must be implemented for any type that is used as a query argument like `#[route("/?:query")]`.
///
/// **This trait is automatically implemented for any types that implement [`Display`].**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromQuerySegment must be implemented for any types you use in the query segment
/// // When you don't spread the query, you can parse multiple values form the query
/// // This url will be in the format `/?query=123&other=456`
/// #[route("/?:query&:other")]
/// Home {
/// query: CustomQuery,
/// other: i32,
/// },
/// }
///
/// // We can derive Default for CustomQuery
/// // If the router fails to parse the query value, it will use the default value instead
/// #[derive(Default, Clone, PartialEq, Debug)]
/// struct CustomQuery {
/// count: i32,
/// }
///
/// // We implement FromStr for CustomQuery so that FromQuerySegment is implemented automatically
/// impl std::str::FromStr for CustomQuery {
/// type Err = <i32 as std::str::FromStr>::Err;
///
/// fn from_str(query: &str) -> Result<Self, Self::Err> {
/// Ok(CustomQuery {
/// count: query.parse()?,
/// })
/// }
/// }
///
/// // We also need to implement Display for CustomQuery so that ToQueryArgument is implemented automatically
/// impl std::fmt::Display for CustomQuery {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}", self.count)
/// }
/// }
///
/// # #[component]
/// # fn Home(query: CustomQuery, other: i32) -> Element {
/// # unimplemented!()
/// # }
/// ```
pub trait ToQueryArgument<T = ()> {
/// Display the query argument as a string.
fn display_query_argument(
&self,
query_name: &str,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result;
}

impl<T> ToQueryArgument for T
where
T: Display,
{
fn display_query_argument(
&self,
query_name: &str,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(f, "{}={}", query_name, self)
}
}

impl<T: Display> ToQueryArgument<OptionMarker> for Option<T> {
fn display_query_argument(
&self,
query_name: &str,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
if let Some(value) = self {
write!(f, "{}={}", query_name, value)
} else {
Ok(())
}
}
}

/// A type that implements [`ToQueryArgument`] along with the query name. This type implements Display and can be used to format the query argument into a string.
pub struct DisplayQueryArgument<'a, T, M = ()> {
/// The query name.
query_name: &'a str,
/// The value to format.
value: &'a T,
/// The `ToQueryArgument` marker type, which can be used to differentiate between different types of query arguments.
_marker: std::marker::PhantomData<M>,
}

impl<'a, T, M> DisplayQueryArgument<'a, T, M> {
/// Create a new `DisplayQueryArgument`.
pub fn new(query_name: &'a str, value: &'a T) -> Self {
Self {
query_name,
value,
_marker: std::marker::PhantomData,
}
}
}

impl<T: ToQueryArgument<M>, M> Display for DisplayQueryArgument<'_, T, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.display_query_argument(self.query_name, f)
}
}

/// Something that can be created from an entire hash fragment. This must be implemented for any type that is used as a hash fragment like `#[route("/#:hash_fragment")]`.
///
///
Expand Down Expand Up @@ -406,7 +538,8 @@ where
for segment in self {
write!(f, "/")?;
let segment = segment.to_string();
let encoded = urlencoding::encode(&segment);
let encoded =
percent_encoding::utf8_percent_encode(&segment, crate::query_sets::PATH_ASCII_SET);
write!(f, "{}", encoded)?;
}
Ok(())
Expand Down
Loading
Loading