Skip to content

Commit

Permalink
support escaping '{' and '}'
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraheemdev committed Mar 10, 2024
1 parent f534ddc commit 9de00b8
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 69 deletions.
32 changes: 20 additions & 12 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::escape::{UnescapedRef, UnscapedRoute};
use crate::tree::{denormalize_params, Node};

use std::fmt;
Expand All @@ -15,9 +16,9 @@ pub enum InsertError {
///
/// Prefixes are allowed before a parmeter, but not after it. For example,
/// `/foo-{bar}` is a valid route, but `/{bar}-foo` is not.
InvalidParamSegment,
/// Parameters must be registered with a valid name, and matching braces.
InvalidParam,
/// Parameters must be registered with a valid name.
InvalidParamName,
/// Catch-all parameters are only allowed at the end of a path.
InvalidCatchAll,
}
Expand All @@ -32,8 +33,10 @@ impl fmt::Display for InsertError {
with
)
}
Self::InvalidParam => write!(f, "only one parameter is allowed per path segment"),
Self::InvalidParamName => write!(f, "parameters must be registered with a valid name"),
Self::InvalidParamSegment => {
write!(f, "only one parameter is allowed per path segment")
}
Self::InvalidParam => write!(f, "parameters must be registered with a valid name"),
Self::InvalidCatchAll => write!(
f,
"catch-all parameters are only allowed at the end of a route"
Expand All @@ -45,20 +48,25 @@ impl fmt::Display for InsertError {
impl std::error::Error for InsertError {}

impl InsertError {
pub(crate) fn conflict<T>(route: &[u8], prefix: &[u8], current: &Node<T>) -> Self {
pub(crate) fn conflict<T>(
route: &UnscapedRoute,
prefix: UnescapedRef<'_>,
current: &Node<T>,
) -> Self {
let mut route = route.clone();

// The new route would have had to replace the current node in the tree.
if prefix == current.prefix {
let mut route = route.to_owned();
if prefix.inner() == current.prefix.inner() {
denormalize_params(&mut route, &current.param_remapping);
return InsertError::Conflict {
with: String::from_utf8(route).unwrap(),
with: String::from_utf8(route.into_inner()).unwrap(),
};
}

let mut route = route[..route.len() - prefix.len()].to_owned();
route.truncate(route.len() - prefix.len());

if !route.ends_with(&current.prefix) {
route.extend_from_slice(&current.prefix);
route.append(&current.prefix);
}

let mut last = current;
Expand All @@ -68,14 +76,14 @@ impl InsertError {

let mut current = current.children.first();
while let Some(node) = current {
route.extend_from_slice(&node.prefix);
route.append(&node.prefix);
current = node.children.first();
}

denormalize_params(&mut route, &last.param_remapping);

InsertError::Conflict {
with: String::from_utf8(route).unwrap(),
with: String::from_utf8(route.into_inner()).unwrap(),
}
}
}
Expand Down
176 changes: 176 additions & 0 deletions src/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use std::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 {
inner: Vec<u8>,
escaped: Vec<usize>,
}

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

while let Some(&c) = inner.get(i) {
if (c == b'{' && inner.get(i + 1) == Some(&b'{'))
|| (c == b'}' && inner.get(i + 1) == Some(&b'}'))
{
inner.remove(i);
escaped.push(i);
}

i += 1;
}

UnscapedRoute { inner, escaped }
}

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

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

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

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

/// Replaces the characters in the given range.
pub fn splice(
&mut self,
range: Range<usize>,
replace: Vec<u8>,
) -> impl Iterator<Item = u8> + '_ {
// update the escaped indices
let offset = (range.len() as isize) - (replace.len() as isize);
for i in &mut self.escaped {
if *i > range.start {
*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) {
for i in &other.escaped {
self.escaped.push(self.inner.len() + i);
}

self.inner.extend_from_slice(&other.inner);
}

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

/// Returns a reference to this route.
pub fn as_ref(&self) -> UnescapedRef<'_> {
UnescapedRef {
inner: &self.inner,
escaped: &self.escaped,
offset: 0,
}
}

/// Returns a reference to the inner slice.
pub fn inner(&self) -> &[u8] {
&self.inner
}

/// Returns the inner slice.
pub fn into_inner(self) -> Vec<u8> {
self.inner
}
}

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

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

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

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

UnscapedRoute {
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))
}

/// 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,
}
}

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

/// Returns a reference to the inner slice.
pub fn inner(&self) -> &[u8] {
&self.inner
}
}

impl<'a> std::ops::Deref for UnescapedRef<'a> {
type Target = &'a [u8];

fn deref(&self) -> &Self::Target {
&self.inner
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
#![deny(rust_2018_idioms, clippy::all)]

mod error;
mod escape;
mod params;
mod router;
mod tree;
Expand Down
Loading

0 comments on commit 9de00b8

Please sign in to comment.