windmark 0.9.0

An elegant and highly performant async Gemini server framework
Documentation
use crate::{context::Parameters, handler::RouteResponse};
use std::{borrow::Cow, sync::Arc};

#[derive(Clone, Default)]
pub(super) struct Routes {
  exact:         matchit::Router<Arc<dyn RouteResponse>>,
  folded:        Option<matchit::Router<Arc<dyn RouteResponse>>>,
  registrations: Vec<(String, Arc<dyn RouteResponse>)>,
}

pub(super) struct MatchedRoute<'router> {
  pub value:      &'router Arc<dyn RouteResponse>,
  pub parameters: Parameters,
}

impl Routes {
  pub fn insert(
    &mut self,
    path: &str,
    handler: Arc<dyn RouteResponse>,
  ) -> Result<(), matchit::InsertError> {
    let path = normalize_pattern(path);
    let mut folded = self.folded.clone();

    if let Some(routes) = &mut folded {
      routes.insert(fold_pattern(&path), handler.clone())?;
    }

    self.exact.insert(path.clone(), handler.clone())?;
    self.registrations.push((path, handler));

    self.folded = folded;

    Ok(())
  }

  pub fn enable_case_insensitive(
    &mut self,
  ) -> Result<(), matchit::InsertError> {
    if self.folded.is_some() {
      return Ok(());
    }

    let mut routes = matchit::Router::new();

    for (path, handler) in &self.registrations {
      routes.insert(fold_pattern(path), handler.clone())?;
    }

    self.folded = Some(routes);

    Ok(())
  }

  pub fn disable_case_insensitive(&mut self) { self.folded = None; }

  pub fn contains(&self, path: &str) -> bool {
    let path = normalize_path(path, None);

    self.folded.as_ref().map_or_else(
      || self.exact.at(&path).is_ok(),
      |routes| routes.at(&path.to_ascii_lowercase()).is_ok(),
    )
  }

  pub fn at(
    &self,
    path: &str,
  ) -> Result<MatchedRoute<'_>, matchit::MatchError> {
    let mut offsets = Vec::new();
    let mut lookup = normalize_path(path, Some(&mut offsets));
    let routes = self.folded.as_ref().map_or(&self.exact, |routes| {
      lookup.to_mut().make_ascii_lowercase();

      routes
    });
    let matched = routes.at(&lookup)?;

    Ok(MatchedRoute {
      value:      matched.value,
      parameters: Parameters::from_lookup(
        &matched.params,
        &lookup,
        path,
        &offsets,
      ),
    })
  }
}

fn normalize_pattern(path: &str) -> String {
  let mut normalized = String::with_capacity(path.len());

  for segment in path.split_inclusive('/') {
    let parameter = segment.find([':', '*']).unwrap_or(segment.len());

    normalized.push_str(&normalize_path(&segment[..parameter], None));
    normalized.push_str(&segment[parameter..]);
  }

  normalized
}

fn normalize_path<'path>(
  path: &'path str,
  mut offsets: Option<&mut Vec<usize>>,
) -> Cow<'path, str> {
  if !path.contains('%') {
    return Cow::Borrowed(path);
  }

  let bytes = path.as_bytes();
  let mut normalized = Vec::with_capacity(bytes.len());
  let mut position = 0;

  if let Some(offsets) = &mut offsets {
    offsets.reserve(bytes.len() + 1);
    offsets.push(0);
  }

  while position < bytes.len() {
    if bytes[position] == b'%' {
      if let Some(digits) = bytes.get(position + 1..position + 3) {
        if let (Some(high), Some(low)) = (
          (digits[0] as char).to_digit(16),
          (digits[1] as char).to_digit(16),
        ) {
          let decoded = u8::try_from(high * 16 + low)
            .expect("two hex digits fit in a byte");

          if decoded.is_ascii_alphanumeric() || b"-._~".contains(&decoded) {
            normalized.push(decoded);

            if let Some(offsets) = &mut offsets {
              offsets.push(position + 3);
            }
          } else {
            normalized.extend([
              b'%',
              digits[0].to_ascii_uppercase(),
              digits[1].to_ascii_uppercase(),
            ]);

            if let Some(offsets) = &mut offsets {
              offsets.extend([position + 1, position + 2, position + 3]);
            }
          }

          position += 3;

          continue;
        }
      }
    }

    normalized.push(bytes[position]);

    if let Some(offsets) = &mut offsets {
      offsets.push(position + 1);
    }

    position += 1;
  }

  Cow::Owned(
    String::from_utf8(normalized)
      .expect("normalising ASCII escapes preserves UTF-8"),
  )
}

fn fold_pattern(path: &str) -> String {
  let mut folded = String::with_capacity(path.len());

  for segment in path.split_inclusive('/') {
    let parameter = segment.find([':', '*']).unwrap_or(segment.len());

    folded.push_str(&segment[..parameter].to_ascii_lowercase());
    folded.push_str(&segment[parameter..]);
  }

  folded
}