use std::borrow::Cow;
use std::ops::ControlFlow;
use axum::http::{HeaderValue, Method};
use rustc_hash::FxHashMap;
use super::path::{decode_multi_segment, Composite, MountedPath, Part};
use super::rule::RouteMethod;
use super::{PathParams, RouteEntry};
pub(super) struct Routes {
pub(super) tables: Vec<PathTable>,
verbs: FxHashMap<String, Index>,
plain: Index,
by_path: FxHashMap<String, usize>,
single: Vec<matchit::Router<()>>,
}
struct Index {
nodes: Vec<Node>,
empty_last: bool,
}
fn empty_last_matches() -> bool {
static MATCHES: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*MATCHES.get_or_init(|| {
let mut router = matchit::Router::new();
router.insert("/m/{key}", ()).is_ok() && router.at("/m/").is_ok()
})
}
#[derive(Default)]
struct Node {
literals: FxHashMap<String, usize>,
variable: Option<usize>,
rest: Option<usize>,
table: Option<usize>,
}
impl Default for Index {
fn default() -> Self {
Self {
nodes: vec![Node::default()],
empty_last: empty_last_matches(),
}
}
}
impl Index {
fn insert(&mut self, path: &str, table: usize) {
let slot = self.slot(path);
debug_assert!(slot.is_none(), "{path}");
*slot = Some(table);
}
fn holds(path: &str) -> bool {
let Some(segments) = path.strip_prefix('/') else {
return false;
};
let mut segments = segments.split('/').peekable();
while let Some(segment) = segments.next() {
let last = segments.peek().is_none();
let variable = segment
.strip_prefix('{')
.and_then(|inner| inner.strip_suffix('}'));
match variable {
Some(name) if name.starts_with('*') && !last => return false,
Some(name) if name.contains(['{', '}']) => return false,
Some(_) => {}
None if segment.contains(['{', '}']) => return false,
None => {}
}
}
true
}
fn slot(&mut self, path: &str) -> &mut Option<usize> {
let mut node = 0;
let segments = path.strip_prefix('/').unwrap_or(path).split('/');
for segment in segments {
if segment.starts_with("{*") {
return &mut self.nodes[node].rest;
}
let next = self.nodes.len();
let child = if segment.starts_with('{') {
*self.nodes[node].variable.get_or_insert(next)
} else {
*self.nodes[node]
.literals
.entry(segment.to_owned())
.or_insert(next)
};
if child == next {
self.nodes.push(Node::default());
}
node = child;
}
&mut self.nodes[node].table
}
fn first(&self, path: &str) -> Option<usize> {
match self.each(path, ControlFlow::Break) {
ControlFlow::Break(table) => Some(table),
ControlFlow::Continue(()) => None,
}
}
fn each<B>(
&self,
path: &str,
mut found: impl FnMut(usize) -> ControlFlow<B>,
) -> ControlFlow<B> {
match path.strip_prefix('/') {
Some(rest) => self.visit(0, Some(rest), &mut found),
None => ControlFlow::Continue(()),
}
}
fn visit<B>(
&self,
node: usize,
rest: Option<&str>,
found: &mut impl FnMut(usize) -> ControlFlow<B>,
) -> ControlFlow<B> {
let empty_last = self.empty_last;
let node = &self.nodes[node];
let Some(rest) = rest else {
return node.table.map_or(ControlFlow::Continue(()), found);
};
let (segment, tail) = match rest.split_once('/') {
Some((segment, tail)) => (segment, Some(tail)),
None => (rest, None),
};
if let Some(&next) = node.literals.get(segment) {
self.visit(next, tail, found)?;
}
if let Some(next) = node
.variable
.filter(|_| !segment.is_empty() || tail.is_some() || empty_last)
{
self.visit(next, tail, found)?;
}
match node.rest.filter(|_| !rest.is_empty()) {
Some(table) => found(table),
None => ControlFlow::Continue(()),
}
}
}
pub(super) struct PathTable {
pub(super) path: String,
pub(super) captures: Vec<String>,
bindings: Vec<Binding>,
plain: bool,
}
struct Binding {
entry: RouteEntry,
names: Option<Vec<String>>,
verb: Option<Verb>,
template: Option<Template>,
unbound: Vec<usize>,
multi_segment: Vec<usize>,
composites: Vec<Composite>,
literals: Vec<(usize, String)>,
}
struct Template {
raw: Vec<String>,
segments: Vec<String>,
start: usize,
}
impl Template {
fn of(mount: &MountedPath) -> Option<Self> {
let raw = mount.last_template.clone()?;
let segments = raw
.iter()
.map(|segment| normalize_escapes(segment).into_owned())
.collect();
Some(Self {
raw,
segments,
start: mount.last_segment,
})
}
}
impl Binding {
fn constrained(&self) -> bool {
!self.literals.is_empty() || self.template.is_some()
}
fn fits(&self, path: &str) -> bool {
let mut segments = path.split('/');
let mut next = 0;
for (at, literal) in &self.literals {
let segment = segments.nth(at - next);
next = at + 1;
if segment.is_none_or(|segment| !same_octets(segment, literal)) {
return false;
}
}
let Some(template) = &self.template else {
return true;
};
let mut at = 0;
for _ in 0..template.start {
match path[at..].find('/') {
Some(slash) => at += slash + 1,
None => return false,
}
}
let end = match (&self.verb, split_verb(path)) {
(Some(_), Some((rest, _))) => rest.len(),
(Some(_), None) => return false,
(None, _) => path.len(),
};
let Some(value) = path.get(at..end) else {
return false;
};
follows(&template.segments, Some(value).filter(|v| !v.is_empty()))
}
}
struct Verb {
raw: String,
decoded: String,
multi_segment: String,
empty_ok: bool,
}
#[derive(Clone)]
pub(super) enum Choice {
Route { table: usize, index: usize },
MethodNotAllowed { table: usize, allow: HeaderValue },
NotFound,
}
pub(super) fn split_verb(path: &str) -> Option<(&str, &str)> {
let start = path.rfind('/').map_or(0, |slash| slash + 1);
let colon = start + path[start..].find(':')?;
Some(path.split_at(colon))
}
fn normalized_bytes(text: &str) -> impl Iterator<Item = u8> + '_ {
let mut digits = 0u8;
text.bytes().map(move |byte| {
if digits > 0 {
digits -= 1;
if byte.is_ascii_hexdigit() {
byte.to_ascii_uppercase()
} else {
byte
}
} else {
if byte == b'%' {
digits = 2;
}
byte
}
})
}
pub(super) fn normalize_escapes(text: &str) -> Cow<'_, str> {
if normalized_bytes(text).eq(text.bytes()) {
return Cow::Borrowed(text);
}
Cow::Owned(
String::from_utf8(normalized_bytes(text).collect())
.expect("upper-casing ASCII letters keeps UTF-8 valid"),
)
}
fn same_octets(received: &str, normalized: &str) -> bool {
received.len() == normalized.len() && normalized_bytes(received).eq(normalized.bytes())
}
pub(super) struct Claim<'a> {
pub(super) method: &'a RouteMethod,
pub(super) verb: Option<&'a str>,
pub(super) template: Option<&'a [String]>,
pub(super) literals: &'a [(usize, String)],
}
pub(super) fn clash(claim: &Claim<'_>, other: &Claim<'_>) -> bool {
claim.verb.map(normalize_escapes) == other.verb.map(normalize_escapes)
&& normalized(claim.template) == normalized(other.template)
&& claim.literals.len() == other.literals.len()
&& claim
.literals
.iter()
.zip(other.literals)
.all(|((at, a), (other_at, b))| {
at == other_at && normalize_escapes(a) == normalize_escapes(b)
})
&& (claim.method == other.method
|| *claim.method == RouteMethod::Any
|| *other.method == RouteMethod::Any)
}
fn normalized(template: Option<&[String]>) -> Option<Vec<Cow<'_, str>>> {
template.map(|segments| segments.iter().map(|s| normalize_escapes(s)).collect())
}
fn normalized_literals(literals: &[(usize, String)]) -> Vec<(usize, String)> {
literals
.iter()
.map(|(at, literal)| (*at, normalize_escapes(literal).into_owned()))
.collect()
}
fn positions(marked: &[bool]) -> Vec<usize> {
marked
.iter()
.enumerate()
.filter_map(|(position, &bare)| bare.then_some(position))
.collect()
}
fn follows(template: &[String], value: Option<&str>) -> bool {
match template.split_first() {
None => value.is_none(),
Some((first, rest)) if first == "**" => {
let mut left = value;
loop {
if follows(rest, left) {
return true;
}
match left {
Some(segments) => left = segments.split_once('/').map(|(_, tail)| tail),
None => return false,
}
}
}
Some((first, rest)) => value.is_some_and(|segments| {
let (segment, tail) = match segments.split_once('/') {
Some((segment, tail)) => (segment, Some(tail)),
None => (segments, None),
};
let fits = if first == "*" {
!segment.is_empty()
} else {
same_octets(segment, first)
};
fits && follows(rest, tail)
}),
}
}
impl Routes {
pub(super) fn new(tables: Vec<PathTable>) -> Self {
let mut verbs: FxHashMap<String, Index> = FxHashMap::default();
let mut plain = Index::default();
let mut by_path =
FxHashMap::with_capacity_and_hasher(tables.len(), rustc_hash::FxBuildHasher);
let mut single = Vec::with_capacity(tables.len());
for (index, table) in tables.iter().enumerate() {
let mut alone = matchit::Router::new();
let inserted = alone.insert(table.path.as_str(), ());
debug_assert!(inserted.is_ok(), "{}: {inserted:?}", table.path);
single.push(alone);
by_path.insert(table.path.clone(), index);
if table.plain {
plain.insert(&table.path, index);
}
let mut seen: Vec<&str> = Vec::new();
for verb in table.bindings.iter().filter_map(|b| b.verb.as_ref()) {
if seen.contains(&verb.raw.as_str()) {
continue;
}
seen.push(&verb.raw);
verbs
.entry(verb.raw.clone())
.or_default()
.insert(&table.path, index);
}
}
Self {
tables,
verbs,
plain,
by_path,
single,
}
}
pub(super) fn table_at(&self, router_path: &str) -> Option<usize> {
self.by_path.get(router_path).copied()
}
pub(super) fn params_of<'r, 'p>(
&'r self,
table: usize,
path: &'p str,
) -> Option<matchit::Params<'r, 'p>> {
self.single[table].at(path).ok().map(|found| found.params)
}
fn answer(
&self,
index: &Index,
first: usize,
method: &Method,
path: &str,
admits: impl Fn(&Binding) -> bool,
ranks: &impl Fn(usize) -> bool,
) -> Option<Choice> {
let mut not_allowed = None;
match self.tables[first].choose(first, method, &admits) {
Some(choice @ Choice::MethodNotAllowed { .. }) => not_allowed = Some(choice),
Some(choice) => return Some(choice),
None => {}
}
let answered = index.each(path, |table| {
if table == first {
return ControlFlow::Continue(());
}
if !ranks(table) {
return ControlFlow::Break(None);
}
match self.tables[table].choose(table, method, &admits) {
Some(choice @ Choice::MethodNotAllowed { .. }) => {
not_allowed.get_or_insert(choice);
ControlFlow::Continue(())
}
Some(choice) => ControlFlow::Break(Some(choice)),
None => ControlFlow::Continue(()),
}
});
match answered {
ControlFlow::Break(Some(choice)) => Some(choice),
ControlFlow::Break(None) | ControlFlow::Continue(()) => not_allowed,
}
}
pub(super) fn choose(&self, table: usize, method: &Method, path: &str) -> Choice {
self.choose_ranked(table, method, path, &|_| true)
}
pub(super) fn choose_ranked(
&self,
table: usize,
method: &Method,
path: &str,
ranks: &impl Fn(usize) -> bool,
) -> Choice {
let plain = |binding: &Binding| binding.verb.is_none() && binding.fits(path);
if self.tables[table].literal_end() {
if let Some(choice) = self.tables[table].choose(table, method, plain) {
return choice;
}
}
if let Some((rest, verb)) = split_verb(path) {
let verb = normalize_escapes(verb);
if let Some(index) = self.verbs.get(verb.as_ref()) {
if let Some(first) = index
.first(path)
.filter(|&first| first == table || ranks(first))
{
let bound = |binding: &Binding| {
binding.verb.as_ref().is_some_and(|own| {
own.raw == verb && (own.empty_ok || !rest.ends_with('/'))
}) && binding.fits(path)
};
if let Some(choice) = self.answer(index, first, method, path, bound, ranks) {
return choice;
}
}
}
}
let first = if self.tables[table].plain {
table
} else {
match self.plain.first(path) {
Some(first) if ranks(first) => first,
_ => return Choice::NotFound,
}
};
self.answer(&self.plain, first, method, path, plain, ranks)
.unwrap_or(Choice::NotFound)
}
}
pub(super) struct Ranked {
index: Index,
tables: usize,
shared: Vec<Vec<Method>>,
head: Vec<bool>,
}
impl Ranked {
pub(super) fn new(routes: &Routes, others: &[(Option<Method>, String)]) -> Option<Self> {
let tables = routes.tables.len();
let mut index = Index::default();
for (table, mounted) in routes.tables.iter().enumerate() {
index.insert(&mounted.path, table);
}
let mut shared = vec![Vec::new(); tables];
for (method, path) in others {
if !Index::holds(path) {
return None;
}
let slot = index.slot(path);
match (*slot, method) {
(None, _) => *slot = Some(tables),
(Some(table), Some(method)) if table < tables => shared[table].push(method.clone()),
(Some(table), None) if table < tables => return None,
(Some(_), _) => {}
}
}
let head = routes.tables.iter().map(PathTable::binds_head).collect();
Some(Self {
index,
tables,
shared,
head,
})
}
pub(super) fn table(&self, method: &Method, path: &str) -> Option<usize> {
let table = self.index.first(path)?;
if table >= self.tables {
return None;
}
if self.theirs(table, method) {
return None;
}
Some(table)
}
fn theirs(&self, table: usize, method: &Method) -> bool {
let shared = &self.shared[table];
shared.contains(method)
|| (*method == Method::HEAD && !self.head[table] && shared.contains(&Method::GET))
}
pub(super) fn allow(&self, table: usize, allow: HeaderValue) -> HeaderValue {
let shared = &self.shared[table];
if shared.is_empty() {
return allow;
}
let mut value = allow.to_str().unwrap_or_default().to_owned();
let mut add = |method: &str| {
if !value.split(", ").any(|listed| listed == method) {
if !value.is_empty() {
value.push_str(", ");
}
value.push_str(method);
}
};
for method in shared {
add(method.as_str());
}
if shared.contains(&Method::GET) {
add(Method::HEAD.as_str());
}
HeaderValue::from_str(&value).expect("method names are header values")
}
pub(super) fn ranks_first(&self, table: usize, method: &Method, path: &str) -> bool {
let found = self.index.each(path, |id| {
if id >= self.tables || self.theirs(id, method) {
ControlFlow::Break(false)
} else if id == table {
ControlFlow::Break(true)
} else {
ControlFlow::Continue(())
}
});
matches!(found, ControlFlow::Break(true))
}
}
impl PathTable {
pub(super) fn new(mount: MountedPath, entry: RouteEntry) -> Self {
let template = Template::of(&mount);
let unbound = positions(&mount.unbound);
let multi_segment = positions(&mount.multi_segment);
let literals = normalized_literals(&mount.literals);
let MountedPath {
axum,
captures,
verb,
empty_last,
composites,
..
} = mount;
Self {
path: axum,
captures,
plain: verb.is_none(),
bindings: vec![Binding {
entry,
names: None,
verb: verb.map(|raw| Verb::new(raw, empty_last)),
template,
unbound,
multi_segment,
composites,
literals,
}],
}
}
pub(super) fn add(&mut self, mount: MountedPath, entry: RouteEntry) {
let claim = Claim {
method: &entry.http_method,
verb: mount.verb.as_deref(),
template: mount.last_template.as_deref(),
literals: &mount.literals,
};
let taken = self.bindings.iter().any(|existing| {
let existing_claim = Claim {
method: &existing.entry.http_method,
verb: existing.verb.as_ref().map(|v| v.raw.as_str()),
template: existing.template.as_ref().map(|t| t.raw.as_slice()),
literals: &existing.literals,
};
clash(&existing_claim, &claim)
});
if taken {
tracing::error!(
method = entry.http_method.as_str(),
path = %mount.display(),
rpc = %entry.grpc_path,
"HTTP method and path already bound to another RPC; skipping this binding"
);
return;
}
let template = Template::of(&mount);
let unbound = positions(&mount.unbound);
let multi_segment = positions(&mount.multi_segment);
let literals = normalized_literals(&mount.literals);
let names = (mount.captures != self.captures).then_some(mount.captures);
let binding = Binding {
entry,
names,
verb: mount.verb.map(|raw| Verb::new(raw, mount.empty_last)),
template,
unbound,
multi_segment,
literals,
composites: mount.composites,
};
let at = if binding.constrained() {
self.bindings
.iter()
.position(|existing| !existing.constrained())
.unwrap_or(self.bindings.len())
} else {
self.bindings.len()
};
self.plain |= binding.verb.is_none();
self.bindings.insert(at, binding);
}
pub(super) fn binds_head(&self) -> bool {
self.bindings
.iter()
.any(|binding| binding.entry.http_method == RouteMethod::One(Method::HEAD))
}
fn literal_end(&self) -> bool {
!self.path.ends_with('}')
}
pub(super) fn entry(&self, index: usize) -> &RouteEntry {
&self.bindings[index].entry
}
fn choose(
&self,
table: usize,
method: &Method,
answers: impl Fn(&Binding) -> bool,
) -> Option<Choice> {
let mut head: Option<usize> = None;
let mut admitted = 0u64;
let mut answered = false;
for (index, binding) in self.bindings.iter().enumerate() {
if !answers(binding) {
continue;
}
answered = true;
if index < 64 {
admitted |= 1 << index;
}
match &binding.entry.http_method {
RouteMethod::Any => return Some(Choice::Route { table, index }),
RouteMethod::One(bound) if bound == method => {
return Some(Choice::Route { table, index })
}
RouteMethod::One(bound) if *bound == Method::GET && *method == Method::HEAD => {
head.get_or_insert(index);
}
RouteMethod::One(_) => {}
}
}
match head {
Some(index) => Some(Choice::Route { table, index }),
None if answered => Some(Choice::MethodNotAllowed {
table,
allow: self.allow(|index| {
if index < 64 {
admitted & (1 << index) != 0
} else {
answers(&self.bindings[index])
}
}),
}),
None => None,
}
}
fn allow(&self, admitted: impl Fn(usize) -> bool) -> HeaderValue {
let mut value = String::new();
let (mut get, mut head) = (false, false);
for (index, binding) in self.bindings.iter().enumerate() {
if !admitted(index) {
continue;
}
let method = binding.entry.http_method.as_str();
let listed = self.bindings[..index]
.iter()
.enumerate()
.any(|(earlier, b)| b.entry.http_method.as_str() == method && admitted(earlier));
if listed {
continue;
}
if !value.is_empty() {
value.push_str(", ");
}
value.push_str(method);
get |= method == "GET";
head |= method == "HEAD";
}
if get && !head {
value.push_str(", HEAD");
}
HeaderValue::try_from(value).expect("method tokens are valid header value characters")
}
pub(super) fn multi_segment(&self, index: usize) -> &[usize] {
&self.bindings[index].multi_segment
}
pub(super) fn bind_params<'p>(&'p self, index: usize, params: &mut PathParams<'p>) {
let binding = &self.bindings[index];
let base = params
.len()
.checked_sub(self.captures.len())
.expect("the router captures every variable of the path it matched");
if let (Some(verb), Some(last)) = (&binding.verb, self.captures.len().checked_sub(1)) {
let multi = binding.multi_segment.contains(&last);
let value = &mut params[base + last].1;
let len = if multi {
let suffix = verb.multi_segment.len();
value.len().checked_sub(suffix).filter(|&start| {
value
.get(start..)
.is_some_and(|tail| same_octets(tail, &verb.multi_segment))
})
} else {
value.strip_suffix(verb.decoded.as_str()).map(str::len)
};
match (len, value) {
(Some(len), Cow::Borrowed(value)) => *value = &value[..len],
(Some(len), Cow::Owned(value)) => value.truncate(len),
(None, _) => {}
}
}
let matched = params.len();
for composite in &binding.composites {
let mut value = String::new();
for (at, part) in composite.parts.iter().enumerate() {
if at > 0 {
value.push('/');
}
value.push_str(match part {
Part::Literal(literal) => literal,
Part::Capture(position) => ¶ms[base + *position].1,
});
}
params.push((composite.field.as_str(), Cow::Owned(value)));
}
let mut kept = 0;
for at in 0..matched {
let keep = match at.checked_sub(base) {
None => true,
Some(position) if binding.unbound.contains(&position) => false,
Some(position) => {
params[at].0 = binding
.names
.as_ref()
.map_or(&self.captures[position], |names| &names[position])
.as_str();
true
}
};
if keep {
params.swap(kept, at);
kept += 1;
}
}
params.drain(kept..matched);
}
}
impl Verb {
fn new(raw: String, empty_ok: bool) -> Self {
let decoded = percent_encoding::percent_decode_str(&raw)
.decode_utf8_lossy()
.into_owned();
let multi_segment =
normalize_escapes(&String::from_utf8_lossy(&decode_multi_segment(&raw))).into_owned();
Self {
raw: normalize_escapes(&raw).into_owned(),
decoded,
multi_segment,
empty_ok,
}
}
}
#[cfg(test)]
mod tests;