Skip to content

Commit

Permalink
add more passing route escape tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraheemdev committed Mar 10, 2024
1 parent 716cd78 commit b106a74
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::escape::{UnescapedRef, UnscapedRoute};
use crate::escape::{UnescapedRef, UnescapedRoute};
use crate::tree::{denormalize_params, Node};

use std::fmt;
Expand Down Expand Up @@ -49,7 +49,7 @@ impl std::error::Error for InsertError {}

impl InsertError {
pub(crate) fn conflict<T>(
route: &UnscapedRoute,
route: &UnescapedRoute,
prefix: UnescapedRef<'_>,
current: &Node<T>,
) -> Self {
Expand Down
69 changes: 49 additions & 20 deletions src/escape.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use std::ops::Range;
use std::{fmt, ops::Range};

/// An uescaped route that keeps track of the position of escaped characters ('{{' or '}}').
///
/// Note that this type dereferences to `&[u8]`.
#[derive(Clone, Default)]
pub struct UnscapedRoute {
pub struct UnescapedRoute {
inner: Vec<u8>,
escaped: Vec<usize>,
}

impl UnscapedRoute {
impl UnescapedRoute {
/// Unescapes escaped brackets ('{{' or '}}') in a route.
pub fn new(mut inner: Vec<u8>) -> UnscapedRoute {
pub fn new(mut inner: Vec<u8>) -> UnescapedRoute {
let mut escaped = Vec::new();
let mut i = 0;

Expand All @@ -26,30 +26,35 @@ impl UnscapedRoute {
i += 1;
}

UnscapedRoute { inner, escaped }
UnescapedRoute { inner, escaped }
}

/// Returns true if the character at the given index was escaped.
pub fn is_escaped(&self, i: usize) -> bool {
self.escaped.contains(&i)
}

/// Slices the route with `start..`.
pub fn slice_off(&self, start: usize) -> UnscapedRoute {
pub fn slice_off(&self, start: usize) -> UnescapedRoute {
let mut escaped = Vec::new();
for &i in &self.escaped {
if i >= start {
escaped.push(i - start);
}
}

UnscapedRoute {
UnescapedRoute {
inner: self.inner[start..].to_owned(),
escaped,
}
}

/// Slices the route with `..end`.
pub fn slice_until(&self, end: usize) -> UnscapedRoute {
pub fn slice_until(&self, end: usize) -> UnescapedRoute {
let mut escaped = self.escaped.clone();
escaped.retain(|&i| i < end);

UnscapedRoute {
UnescapedRoute {
inner: self.inner[..end].to_owned(),
escaped,
}
Expand All @@ -61,19 +66,22 @@ impl UnscapedRoute {
range: Range<usize>,
replace: Vec<u8>,
) -> impl Iterator<Item = u8> + '_ {
// ignore any escaped characters in the range being replaced
self.escaped.retain(|x| !range.contains(x));

// update the escaped indices
let offset = (range.len() as isize) - (replace.len() as isize);
let offset = (replace.len() as isize) - (range.len() as isize);
for i in &mut self.escaped {
if *i > range.start {
*i = (*i).checked_add_signed(offset).unwrap();
if *i > range.end {
*i = i.checked_add_signed(offset).unwrap();
}
}

self.inner.splice(range, replace)
}

/// Appends another route to the end of this one.
pub fn append(&mut self, other: &UnscapedRoute) {
pub fn append(&mut self, other: &UnescapedRoute) {
for i in &other.escaped {
self.escaped.push(self.inner.len() + i);
}
Expand All @@ -83,6 +91,7 @@ impl UnscapedRoute {

/// Truncates the route to the given length.
pub fn truncate(&mut self, to: usize) {
self.escaped.retain(|&x| x < to);
self.inner.truncate(to);
}

Expand All @@ -106,49 +115,59 @@ impl UnscapedRoute {
}
}

impl std::ops::Deref for UnscapedRoute {
impl std::ops::Deref for UnescapedRoute {
type Target = [u8];

fn deref(&self) -> &Self::Target {
&self.inner
}
}

impl fmt::Debug for UnescapedRoute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UnescapedRoute")
.field("inner", &std::str::from_utf8(&self.inner))
.field("escaped", &self.escaped)
.finish()
}
}

/// A reference to an `UnescapedRoute`.
#[derive(Copy, Clone)]
pub struct UnescapedRef<'a> {
pub inner: &'a [u8],
escaped: &'a [usize],
offset: usize,
offset: isize,
}

impl<'a> UnescapedRef<'a> {
/// Converts this reference into an owned route.
pub fn to_owned(self) -> UnscapedRoute {
pub fn to_owned(self) -> UnescapedRoute {
let mut escaped = Vec::new();
for &i in self.escaped {
if i + self.offset < self.inner.len() {
let i = i.wrapping_add_signed(-self.offset);
if i < self.inner.len() {
escaped.push(i);
}
}

UnscapedRoute {
UnescapedRoute {
escaped,
inner: self.inner.to_owned(),
}
}

/// Returns true if the character at the given index was escaped.
pub fn is_escaped(&self, i: usize) -> bool {
self.escaped.contains(&(i + self.offset))
self.escaped.contains(&(i.wrapping_add_signed(self.offset)))
}

/// Slices the route with `start..`.
pub fn slice_off(&self, start: usize) -> UnescapedRef<'a> {
UnescapedRef {
inner: &self.inner[start..],
escaped: self.escaped,
offset: self.offset + start,
offset: self.offset + (start as isize),
}
}

Expand All @@ -174,3 +193,13 @@ impl<'a> std::ops::Deref for UnescapedRef<'a> {
&self.inner
}
}

impl<'a> fmt::Debug for UnescapedRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UnescapedRef")
.field("inner", &std::str::from_utf8(&self.inner))
.field("escaped", &self.escaped)
.field("offset", &self.offset)
.finish()
}
}
50 changes: 33 additions & 17 deletions src/tree.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::escape::{UnescapedRef, UnscapedRoute};
use crate::escape::{UnescapedRef, UnescapedRoute};
use crate::{InsertError, MatchError, Params};

use std::cell::UnsafeCell;
Expand Down Expand Up @@ -30,7 +30,7 @@ pub struct Node<T> {
value: Option<UnsafeCell<T>>,
pub(crate) param_remapping: ParamRemapping,
pub(crate) node_type: NodeType,
pub(crate) prefix: UnscapedRoute,
pub(crate) prefix: UnescapedRoute,
pub(crate) children: Vec<Self>,
}

Expand All @@ -41,7 +41,7 @@ unsafe impl<T: Sync> Sync for Node<T> {}
impl<T> Node<T> {
pub fn insert(&mut self, route: impl Into<String>, val: T) -> Result<(), InsertError> {
let route = route.into().into_bytes();
let route = UnscapedRoute::new(route);
let route = UnescapedRoute::new(route);
let (route, param_remapping) = normalize_params(route)?;
let mut prefix = route.as_ref();

Expand All @@ -61,7 +61,10 @@ impl<T> Node<T> {
// find the longest common prefix
let len = min(prefix.len(), current.prefix.len());
let common_prefix = (0..len)
.find(|&i| prefix[i] != current.prefix[i])
.find(|&i| {
prefix[i] != current.prefix[i]
|| prefix.is_escaped(i) != current.prefix.is_escaped(i)
})
.unwrap_or(len);

// the common prefix is a substring of the current node's prefix, split the node
Expand Down Expand Up @@ -105,14 +108,21 @@ impl<T> Node<T> {
for mut i in 0..current.indices.len() {
// found a match
if next == current.indices[i] {
// the indice matches literally, but it's actually the start of a wildcard
if matches!(next, b'{' | b'}') && !prefix.is_escaped(0) {
continue;
}

i = current.update_child_priority(i);
current = &mut current.children[i];
continue 'walk;
}
}

// not a wildcard and there is no matching child node, create a new one
if !matches!(next, b'{') && current.node_type != NodeType::CatchAll {
if (!matches!(next, b'{') || prefix.is_escaped(0))
&& current.node_type != NodeType::CatchAll
{
current.indices.push(next);
let mut child = current.add_child(Node::default());
child = current.update_child_priority(child);
Expand Down Expand Up @@ -484,9 +494,11 @@ type ParamRemapping = Vec<Vec<u8>>;

/// Returns `path` with normalized route parameters, and a parameter remapping
/// to store at the leaf node for this route.
///
/// Note that the parameter remapping may contain unescaped characters.
fn normalize_params(
mut path: UnscapedRoute,
) -> Result<(UnscapedRoute, ParamRemapping), InsertError> {
mut path: UnescapedRoute,
) -> Result<(UnescapedRoute, ParamRemapping), InsertError> {
let mut start = 0;
let mut original = ParamRemapping::new();

Expand Down Expand Up @@ -532,7 +544,7 @@ fn normalize_params(
}

/// Restores `route` to it's original, denormalized form.
pub(crate) fn denormalize_params(route: &mut UnscapedRoute, params: &ParamRemapping) {
pub(crate) fn denormalize_params(route: &mut UnescapedRoute, params: &ParamRemapping) {
let mut start = 0;
let mut i = 0;

Expand Down Expand Up @@ -588,6 +600,10 @@ fn find_wildcard(path: UnescapedRef<'_>) -> Result<Option<Range<usize>>, InsertE
for (i, &c) in path.iter().enumerate().skip(start + 2) {
match c {
b'}' => {
if path.is_escaped(i) {
continue;
}

if path.get(i - 1) == Some(&b'*') {
return Err(InsertError::InvalidParam);
}
Expand Down Expand Up @@ -640,7 +656,7 @@ impl<T> Default for Node<T> {
fn default() -> Self {
Self {
param_remapping: ParamRemapping::new(),
prefix: UnscapedRoute::default(),
prefix: UnescapedRoute::default(),
wild_child: false,
node_type: NodeType::Static,
indices: Vec::new(),
Expand Down Expand Up @@ -673,14 +689,14 @@ const _: () = {
.map(|x| std::str::from_utf8(x).unwrap())
.collect::<Vec<_>>();

let mut fmt = f.debug_struct("Node");
fmt.field("value", &value);
fmt.field("prefix", &std::str::from_utf8(&self.prefix));
fmt.field("node_type", &self.node_type);
fmt.field("children", &self.children);
fmt.field("param_names", &param_names);
fmt.field("indices", &indices);
fmt.finish()
f.debug_struct("Node")
.field("value", &value)
.field("prefix", &self.prefix)
.field("node_type", &self.node_type)
.field("children", &self.children)
.field("param_names", &param_names)
.field("indices", &indices)
.finish()
}
}
};
10 changes: 9 additions & 1 deletion tests/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ impl InsertTest {
let mut router = Router::new();
for (route, expected) in self.0 {
let got = router.insert(route, route.to_owned());
assert_eq!(got, expected);
assert_eq!(got, expected, "{route}");
}
}
}
Expand Down Expand Up @@ -218,6 +218,14 @@ fn invalid_param() {
("}}yy{{}}", Ok(())),
("}}yy{{}}{{}}y{{", Ok(())),
("}}yy{{}}{{}}y{{", Err(conflict("}yy{}{}y{"))),
("/{{yy", Ok(())),
("/{yy}", Ok(())),
("/foo", Ok(())),
("/foo/{{", Ok(())),
("/foo/{{/{x}", Ok(())),
("/foo/{ba{{r}", Ok(())),
("/bar/{ba}}r}", Ok(())),
("/xxx/{x{{}}y}", Ok(())),
])
.run()
}
Expand Down
Loading

0 comments on commit b106a74

Please sign in to comment.