From 7e8a7e6449f4640d07dd6ceeb95a9779245ee406 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Mon, 8 Dec 2025 06:23:09 +0800 Subject: [PATCH 1/7] fix: required parameter should not be empty --- src/node.rs | 5 +++++ src/parser.rs | 4 ++++ tests/issues.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/issues.rs diff --git a/src/node.rs b/src/node.rs index 8aff4ed..6a3b1da 100644 --- a/src/node.rs +++ b/src/node.rs @@ -210,6 +210,11 @@ impl Node { }); } } else { + // if it's normal, parameter should not be empty + if k == &Kind::Normal && bytes[0] == b'/' { + return None; + } + // static if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { nodes.iter().find_map(|node| match &node.key { diff --git a/src/parser.rs b/src/parser.rs index 1615d67..9ab4fdb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,6 +1,10 @@ use alloc::{string::ToString, vec::Vec}; use core::{iter::Peekable, str::CharIndices}; +/// Types of path segments. +/// +/// Represents the matching pattern for parameters in URL paths. +/// Different kinds determine how parameters match URL path segments. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum Kind { /// `:` 58 diff --git a/tests/issues.rs b/tests/issues.rs new file mode 100644 index 0000000..f0ebf5a --- /dev/null +++ b/tests/issues.rs @@ -0,0 +1,43 @@ +use path_tree::PathTree; + +#[test] +fn test_44() { + let mut tree = PathTree::new(); + let _ = tree.insert("/test/:me", 0); + let _ = tree.insert("/test/:me?", 1); + let _ = tree.insert("/test/:me/now", 2); + let _ = tree.insert("/test/:me?/now", 3); + + let (value, path) = tree.find("/test/").unwrap(); + assert_eq!(value, &1); + assert_eq!(path.params(), &[("me", "")]); + + let (value, path) = tree.find("/test/now").unwrap(); + assert_eq!(value, &0); + assert_eq!(path.params(), &[("me", "now")]); + + // not found + let result = tree.find("/test//"); + assert!(result.is_none()); + + let (value, path) = tree.find("/test//now").unwrap(); + assert_eq!(value, &3); + assert_eq!(path.params(), &[("me", "")]); + + let (value, path) = tree.find(r"/test/\/now").unwrap(); + assert_eq!(value, &2); + assert_eq!(path.params(), &[("me", r"\")]); + + // trim `/` + let trimed = "/test//now" + .split('/') + .filter(|s| !s.is_empty()) + .collect::>(); + let mut path = trimed.join("/"); + // add lead with `/` + path.insert(0, '/'); + assert_eq!(path, "/test/now"); + let (value, path) = tree.find(&path).unwrap(); + assert_eq!(value, &0); + assert_eq!(path.params(), &[("me", "now")]); +} From 3301efba009901b565a06fc4f0d433b771505f9b Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Mon, 8 Dec 2025 06:36:15 +0800 Subject: [PATCH 2/7] Update tests/issues.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/issues.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/issues.rs b/tests/issues.rs index f0ebf5a..36e6a98 100644 --- a/tests/issues.rs +++ b/tests/issues.rs @@ -29,11 +29,11 @@ fn test_44() { assert_eq!(path.params(), &[("me", r"\")]); // trim `/` - let trimed = "/test//now" + let trimmed = "/test//now" .split('/') .filter(|s| !s.is_empty()) .collect::>(); - let mut path = trimed.join("/"); + let mut path = trimmed.join("/"); // add lead with `/` path.insert(0, '/'); assert_eq!(path, "/test/now"); From e745f090bd00a71ea417defa660575ea41b2bfd4 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Mon, 8 Dec 2025 06:36:23 +0800 Subject: [PATCH 3/7] Update tests/issues.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/issues.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/issues.rs b/tests/issues.rs index 36e6a98..7832ad3 100644 --- a/tests/issues.rs +++ b/tests/issues.rs @@ -34,7 +34,7 @@ fn test_44() { .filter(|s| !s.is_empty()) .collect::>(); let mut path = trimmed.join("/"); - // add lead with `/` + // add leading `/` path.insert(0, '/'); assert_eq!(path, "/test/now"); let (value, path) = tree.find(&path).unwrap(); From 7e7a6f33d099ff764d378b04fa4cc668e61c69d8 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Tue, 9 Dec 2025 02:20:10 +0800 Subject: [PATCH 4/7] fix: when the node is slash, match its static nodes first --- src/lib.rs | 2 +- src/node.rs | 53 ++++++++++++++++++++++++++++++++++++++++--------- tests/issues.rs | 22 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 411fb7c..e0e5142 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,7 +132,7 @@ //! assert_eq!(p.params(), vec![("+1", "v1")]); //! ``` -#![no_std] +// #![no_std] #![forbid(unsafe_code)] #![warn(rust_2018_idioms, unreachable_pub)] diff --git a/src/node.rs b/src/node.rs index 6a3b1da..572b6c1 100644 --- a/src/node.rs +++ b/src/node.rs @@ -339,12 +339,39 @@ impl Node { if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { nodes.iter().find_map(|node| { if let Key::String(s) = &node.key { - let right_length = if is_one_or_more { + let is_slash = is_one_or_more && s.len() == 1 && s[0] == b'/'; + if is_slash { + let r = bytes + .iter() + .enumerate() + .filter_map(|(n, b)| (s[0] == *b).then_some(n)) + .find_map(|n| { + node.nodes0.as_ref().and_then(|nodes| { + nodes.iter().find_map(|node| { + node.find_with( + start + n + 1, + &bytes[n + 1..], + ranges, + ) + .inspect(|_| { + ranges.push(start..start + n); + }) + }) + }) + }); + + if r.is_some() { + return r; + } + } + + let has_right_length = if is_one_or_more { m > s.len() } else { m >= s.len() }; - if right_length { + + if has_right_length { return bytes .iter() .enumerate() @@ -486,15 +513,23 @@ impl fmt::Debug for Node { } } +const KINDS: [u8; 5] = [b'/', b':', b'?', b'+', b'*']; + +#[inline] +fn find_index(c: u8) -> Option { + KINDS.iter().position(|e| *e == c) +} + #[inline] fn compare(a: u8, b: u8) -> Ordering { if a == b { - Ordering::Equal - } else if a == b'/' { - Ordering::Greater - } else if b == b'/' { - Ordering::Less - } else { - a.cmp(&b) + return Ordering::Equal; + } + + match (find_index(a), find_index(b)) { + (Some(m), Some(n)) => m.cmp(&n), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => a.cmp(&b), } } diff --git a/tests/issues.rs b/tests/issues.rs index 7832ad3..018e1c5 100644 --- a/tests/issues.rs +++ b/tests/issues.rs @@ -7,6 +7,9 @@ fn test_44() { let _ = tree.insert("/test/:me?", 1); let _ = tree.insert("/test/:me/now", 2); let _ = tree.insert("/test/:me?/now", 3); + let _ = tree.insert("/test/:this+/now", 4); + let _ = tree.insert("/test/:this+/*", 5); + let _ = tree.insert("/test/:this+/now/*", 6); let (value, path) = tree.find("/test/").unwrap(); assert_eq!(value, &1); @@ -40,4 +43,23 @@ fn test_44() { let (value, path) = tree.find(&path).unwrap(); assert_eq!(value, &0); assert_eq!(path.params(), &[("me", "now")]); + + let (value, path) = tree.find("/test/multiple/paths/now").unwrap(); + assert_eq!(value, &4); + assert_eq!(path.params(), &[("this", "multiple/paths")]); + + let (value, path) = tree.find("/test/multiple/paths/noww").unwrap(); + assert_eq!(value, &5); + assert_eq!(path.params(), &[("this", "multiple"), ("*1", "paths/noww")]); + + let (value, path) = tree.find("/test/multiple/paths/now/12h").unwrap(); + assert_eq!(value, &6); + assert_eq!(path.params(), &[("this", "multiple/paths"), ("*1", "12h")]); + + let (value, path) = tree.find("/test/multiple/paths/today/12h").unwrap(); + assert_eq!(value, &5); + assert_eq!( + path.params(), + &[("this", "multiple"), ("*1", "paths/today/12h")] + ); } From e1b0195996fb04690084b20be7ae5f6c2e4c506a Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Tue, 9 Dec 2025 02:24:48 +0800 Subject: [PATCH 5/7] chore: add no_std back --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e0e5142..411fb7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,7 +132,7 @@ //! assert_eq!(p.params(), vec![("+1", "v1")]); //! ``` -// #![no_std] +#![no_std] #![forbid(unsafe_code)] #![warn(rust_2018_idioms, unreachable_pub)] From 1f69b891c81e9ad8fabde650875264a60561501a Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Tue, 9 Dec 2025 05:36:21 +0800 Subject: [PATCH 6/7] chore: remove redundant variables --- src/node.rs | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/node.rs b/src/node.rs index 572b6c1..f381b91 100644 --- a/src/node.rs +++ b/src/node.rs @@ -164,14 +164,13 @@ impl Node { // parameter if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { - let b = m > 0; nodes .iter() .filter(|node| match node.key { Key::Parameter(pk) if pk == Kind::Normal || pk == Kind::OneOrMore => { - b + m > 0 } _ => true, }) @@ -250,14 +249,13 @@ impl Node { // parameter => `:a:b:c` if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { - let b = m - 1 > 0; nodes .iter() .filter(|node| match node.key { Key::Parameter(pk) if pk == Kind::Normal || pk == Kind::OneOrMore => { - b + m - 1 > 0 } _ => true, }) @@ -271,14 +269,13 @@ impl Node { // parameter => `:a:b?:c?` if k == &Kind::Optional || k == &Kind::OptionalSegment { if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { - let b = m > 0; nodes .iter() .filter(|node| match &node.key { Key::Parameter(pk) if pk == &Kind::Normal || pk == &Kind::OneOrMore => { - b + m > 0 } _ => true, }) @@ -341,7 +338,7 @@ impl Node { if let Key::String(s) = &node.key { let is_slash = is_one_or_more && s.len() == 1 && s[0] == b'/'; if is_slash { - let r = bytes + if let Some(id) = bytes .iter() .enumerate() .filter_map(|(n, b)| (s[0] == *b).then_some(n)) @@ -358,10 +355,9 @@ impl Node { }) }) }) - }); - - if r.is_some() { - return r; + }) + { + return Some(id); } } @@ -513,23 +509,15 @@ impl fmt::Debug for Node { } } -const KINDS: [u8; 5] = [b'/', b':', b'?', b'+', b'*']; - -#[inline] -fn find_index(c: u8) -> Option { - KINDS.iter().position(|e| *e == c) -} - #[inline] fn compare(a: u8, b: u8) -> Ordering { if a == b { - return Ordering::Equal; - } - - match (find_index(a), find_index(b)) { - (Some(m), Some(n)) => m.cmp(&n), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => a.cmp(&b), + Ordering::Equal + } else if a == b'/' { + Ordering::Greater + } else if b == b'/' { + Ordering::Less + } else { + a.cmp(&b) } } From c3e7b371a14a977e64b7c7e96a944c9f209b2a7e Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Sat, 13 Dec 2025 04:29:33 +0800 Subject: [PATCH 7/7] chore: remove redundant variables --- src/node.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/node.rs b/src/node.rs index f381b91..e32946d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -509,15 +509,23 @@ impl fmt::Debug for Node { } } +const KINDS: [u8; 5] = [b'/', b':', b'?', b'+', b'*']; + +#[inline] +fn find_index(c: u8) -> Option { + KINDS.iter().position(|e| *e == c) +} + #[inline] fn compare(a: u8, b: u8) -> Ordering { if a == b { - Ordering::Equal - } else if a == b'/' { - Ordering::Greater - } else if b == b'/' { - Ordering::Less - } else { - a.cmp(&b) + return Ordering::Equal; + } + + match (find_index(a), find_index(b)) { + (Some(m), Some(n)) => m.cmp(&n), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => a.cmp(&b), } }