fix: correct scheme handling in router, and improve matching code by removing regexes (#569)

This commit is contained in:
g-re-g 2023-02-26 07:15:14 -05:00 committed by GitHub
parent 3d50ca32cd
commit 3648af0d9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 130 additions and 179 deletions

View file

@ -35,100 +35,52 @@ pub fn resolve_path<'a>(
}
}
#[cfg(feature = "ssr")]
fn has_scheme(path: &str) -> bool {
use regex::Regex;
lazy_static::lazy_static! {
pub static ref HAS_SCHEME_RE: Regex =
Regex::new(HAS_SCHEME).expect("couldn't compile HAS_SCHEME_RE");
}
HAS_SCHEME_RE.is_match(path)
}
#[cfg(not(feature = "ssr"))]
fn has_scheme(path: &str) -> bool {
let re = js_sys::RegExp::new(HAS_SCHEME, "");
re.test(path)
path.starts_with("//")
|| path.starts_with("tel:")
|| path.starts_with("mailto:")
|| path
.split_once("://")
.map(|(prefix, _)| {
prefix.chars().all(
|c: char| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9'),
)
})
.unwrap_or(false)
}
#[doc(hidden)]
pub fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
let s = replace_trim_path(path, "");
if !s.is_empty() {
if omit_slash || begins_with_query_or_hash(&s) {
s
} else {
format!("/{s}").into()
}
fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
let s = path.trim_start_matches('/').trim_end_matches('/');
if s.is_empty() || omit_slash || begins_with_query_or_hash(&s) {
s.into()
} else {
"".into()
format!("/{s}").into()
}
}
#[doc(hidden)]
pub fn join_paths<'a>(from: &'a str, to: &'a str) -> String {
let from = replace_query(&normalize(from, false));
let from = remove_wildcard(&normalize(from, false));
from + &normalize(to, false)
}
const TRIM_PATH: &str = r#"^/+|/+$"#;
const BEGINS_WITH_QUERY_OR_HASH: &str = r#"^[?#]"#;
const HAS_SCHEME: &str = r#"^(?:[a-z0-9]+:)?//"#;
const QUERY: &str = r#"/*(\*.*)?$"#;
#[cfg(not(feature = "ssr"))]
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
let re = js_sys::RegExp::new(TRIM_PATH, "g");
js_sys::JsString::from(text)
.replace_by_pattern(&re, replace)
.as_string()
.unwrap()
.into()
}
#[cfg(not(feature = "ssr"))]
fn begins_with_query_or_hash(text: &str) -> bool {
let re = js_sys::RegExp::new(BEGINS_WITH_QUERY_OR_HASH, "");
re.test(text)
matches!(text.chars().next(), Some('#') | Some('?'))
}
#[cfg(not(feature = "ssr"))]
fn replace_query(text: &str) -> String {
let re = js_sys::RegExp::new(QUERY, "g");
js_sys::JsString::from(text)
.replace_by_pattern(&re, "")
.as_string()
.unwrap()
fn remove_wildcard(text: &str) -> String {
text.split_once('*')
.map(|(prefix, _)| prefix.trim_end_matches('/'))
.unwrap_or(text)
.to_string()
}
#[cfg(feature = "ssr")]
fn replace_trim_path<'a>(text: &'a str, replace: &str) -> Cow<'a, str> {
use regex::Regex;
lazy_static::lazy_static! {
pub static ref TRIM_PATH_RE: Regex =
Regex::new(TRIM_PATH).expect("couldn't compile TRIM_PATH_RE");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_query_string_with_opening_slash() {
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
}
TRIM_PATH_RE.replace(text, replace)
}
#[cfg(feature = "ssr")]
fn begins_with_query_or_hash(text: &str) -> bool {
use regex::Regex;
lazy_static::lazy_static! {
pub static ref BEGINS_WITH_QUERY_OR_HASH_RE: Regex =
Regex::new(BEGINS_WITH_QUERY_OR_HASH).expect("couldn't compile BEGINS_WITH_HASH_RE");
}
BEGINS_WITH_QUERY_OR_HASH_RE.is_match(text)
}
#[cfg(feature = "ssr")]
fn replace_query(text: &str) -> String {
use regex::Regex;
lazy_static::lazy_static! {
pub static ref QUERY_RE: Regex =
Regex::new(QUERY).expect("couldn't compile QUERY_RE");
}
QUERY_RE.replace(text, "").into_owned()
}

View file

@ -1,107 +1,106 @@
use cfg_if::cfg_if;
// Test cases drawn from Solid Router
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::{normalize, resolve_path};
use leptos_router::resolve_path;
#[test]
fn normalize_query_string_with_opening_slash() {
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
}
#[test]
fn resolve_path_should_normalize_base_arg() {
assert_eq!(resolve_path("base", "", None), Some("/base".into()));
}
#[test]
fn resolve_path_should_normalize_path_arg() {
assert_eq!(resolve_path("", "path", None), Some("/path".into()));
}
#[test]
fn resolve_path_should_normalize_from_arg() {
assert_eq!(resolve_path("", "", Some("from")), Some("/from".into()));
}
#[test]
fn resolve_path_should_return_default_when_all_empty() {
assert_eq!(resolve_path("", "", None), Some("/".into()));
}
#[test]
fn resolve_path_should_resolve_root_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/", Some("/base/foo")),
Some("/base".into())
);
}
#[test]
fn resolve_path_should_resolve_rooted_paths_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/bar", Some("/base/foo")),
Some("/base/bar".into())
);
}
#[test]
fn resolve_path_should_resolve_empty_path_against_from() {
assert_eq!(
resolve_path("/base", "", Some("/base/foo")),
Some("/base/foo".into())
);
}
#[test]
fn resolve_path_should_resolve_relative_paths_against_from() {
assert_eq!(
resolve_path("/base", "bar", Some("/base/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_prepend_base_if_from_doesnt_start_with_it() {
assert_eq!(
resolve_path("/base", "bar", Some("/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_test_start_of_from_against_base_case_insensitive() {
assert_eq!(
resolve_path("/base", "bar", Some("BASE/foo")),
Some("/BASE/foo/bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search_and_base() {
assert_eq!(
resolve_path("/base", "/?foo=bar", Some("/base/page")),
Some("/base?foo=bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search() {
assert_eq!(
resolve_path("", "/?foo=bar", None),
Some("/?foo=bar".into())
);
}
#[test]
fn preserve_spaces() {
assert_eq!(
resolve_path(" foo ", " bar baz ", None),
Some("/ foo / bar baz ".into())
);
}
}
#[test]
fn resolve_path_should_normalize_base_arg() {
assert_eq!(resolve_path("base", "", None), Some("/base".into()));
}
#[test]
fn resolve_path_should_normalize_path_arg() {
assert_eq!(resolve_path("", "path", None), Some("/path".into()));
}
#[test]
fn resolve_path_should_normalize_from_arg() {
assert_eq!(resolve_path("", "", Some("from")), Some("/from".into()));
}
#[test]
fn resolve_path_should_return_default_when_all_empty() {
assert_eq!(resolve_path("", "", None), Some("/".into()));
}
#[test]
fn resolve_path_should_resolve_root_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/", Some("/base/foo")),
Some("/base".into())
);
}
#[test]
fn resolve_path_should_resolve_rooted_paths_against_base_and_ignore_from() {
assert_eq!(
resolve_path("/base", "/bar", Some("/base/foo")),
Some("/base/bar".into())
);
}
#[test]
fn resolve_path_should_resolve_empty_path_against_from() {
assert_eq!(
resolve_path("/base", "", Some("/base/foo")),
Some("/base/foo".into())
);
}
#[test]
fn resolve_path_should_resolve_relative_paths_against_from() {
assert_eq!(
resolve_path("/base", "bar", Some("/base/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_prepend_base_if_from_doesnt_start_with_it() {
assert_eq!(
resolve_path("/base", "bar", Some("/foo")),
Some("/base/foo/bar".into())
);
}
#[test]
fn resolve_path_should_test_start_of_from_against_base_case_insensitive() {
assert_eq!(
resolve_path("/base", "bar", Some("BASE/foo")),
Some("/BASE/foo/bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search_and_base() {
assert_eq!(
resolve_path("/base", "/?foo=bar", Some("/base/page")),
Some("/base?foo=bar".into())
);
}
#[test]
fn resolve_path_should_work_with_rooted_search() {
assert_eq!(
resolve_path("", "/?foo=bar", None),
Some("/?foo=bar".into())
);
}
#[test]
fn preserve_spaces() {
assert_eq!(
resolve_path(" foo ", " bar baz ", None),
Some("/ foo / bar baz ".into())
);
}
#[test]
fn cannot_resolve_if_path_has_scheme() {
assert_eq!(resolve_path("", "http://example.com", None), None);
assert_eq!(resolve_path("", "https://example.com", None), None);
assert_eq!(resolve_path("", "example://google.com", None), None);
assert_eq!(resolve_path("", "tel:+15555555555", None), None);
assert_eq!(resolve_path("", "mailto:name@example.com", None), None);
assert_eq!(resolve_path("", "//relative-protocol", None), None);
}