windmark 0.9.0

An elegant and highly performant async Gemini server framework
Documentation
use url::Url;

pub(super) fn parse_uri(request: &str) -> Result<Url, String> {
  if !valid_uri_characters(request, false) {
    return Err("request URI contains invalid syntax".into());
  }

  if let Some((_, remainder)) = request.split_once(':') {
    let path_and_query = if let Some(authority) = remainder.strip_prefix("//") {
      let end = authority.find(['/', '?']).unwrap_or(authority.len());

      if authority[..end].contains('@') {
        return Err("request URI must not contain userinfo".into());
      }

      &authority[end..]
    } else {
      remainder
    };

    if path_and_query.contains(['[', ']']) {
      return Err("request URI contains an unescaped bracket".into());
    }
  }

  let url = Url::parse(request).map_err(|error| error.to_string())?;

  if !url.username().is_empty() || url.password().is_some() {
    return Err("request URI must not contain userinfo".into());
  }

  if url.scheme() == "gemini" && url.host_str().is_none_or(str::is_empty) {
    return Err("Gemini request URI requires a host".into());
  }

  Ok(url)
}

fn valid_uri_characters(request: &str, allow_fragment: bool) -> bool {
  let bytes = request.as_bytes();
  let mut position = 0;

  while position < bytes.len() {
    let byte = bytes[position];

    if byte == b'%' {
      if bytes
        .get(position + 1..position + 3)
        .is_none_or(|digits| !digits.iter().all(u8::is_ascii_hexdigit))
      {
        return false;
      }

      position += 3;
    } else if byte.is_ascii_alphanumeric()
      || b":/?[]@!$&'()*+,;=-._~".contains(&byte)
      || (allow_fragment && byte == b'#')
    {
      position += 1;
    } else {
      return false;
    }
  }

  true
}

pub(super) fn valid_uri_reference(reference: &str) -> bool {
  let first_segment =
    reference.split(['/', '?', '#']).next().unwrap_or_default();

  if let Some((scheme, _)) = first_segment.split_once(':') {
    if !scheme
      .as_bytes()
      .first()
      .is_some_and(u8::is_ascii_alphabetic)
      || !scheme
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || b"+-.".contains(&byte))
    {
      return false;
    }
  }

  let remainder = reference
    .split_once(':')
    .filter(|(prefix, _)| !prefix.contains(['/', '?', '#']))
    .map_or(reference, |(_, remainder)| remainder);
  let path = if let Some(authority) = remainder.strip_prefix("//") {
    let end = authority.find(['/', '?', '#']).unwrap_or(authority.len());

    if !valid_authority(&authority[..end]) {
      return false;
    }

    &authority[end..]
  } else {
    remainder
  };

  valid_uri_characters(reference, true)
    && !path.contains(['[', ']'])
    && reference.matches('#').count() <= 1
}

fn valid_authority(authority: &str) -> bool {
  let host_and_port = if let Some((userinfo, host)) = authority.split_once('@')
  {
    if userinfo.contains(['[', ']']) {
      return false;
    }

    host
  } else {
    authority
  };

  if let Some(literal) = host_and_port.strip_prefix('[') {
    let Some((address, suffix)) = literal.split_once(']') else {
      return false;
    };
    let valid_address = address.strip_prefix(['v', 'V']).map_or_else(
      || address.parse::<std::net::Ipv6Addr>().is_ok(),
      |future| {
        future.split_once('.').is_some_and(|(version, address)| {
          !version.is_empty()
            && version.bytes().all(|byte| byte.is_ascii_hexdigit())
            && !address.is_empty()
            && address.bytes().all(|byte| {
              byte.is_ascii_alphanumeric()
                || b"-._~!$&'()*+,;=:".contains(&byte)
            })
        })
      },
    );

    return valid_address
      && (suffix.is_empty()
        || suffix
          .strip_prefix(':')
          .is_some_and(|port| port.bytes().all(|byte| byte.is_ascii_digit())));
  }

  let (host, port) = host_and_port
    .rsplit_once(':')
    .unwrap_or((host_and_port, ""));

  !host.contains(['@', ':', '[', ']'])
    && port.bytes().all(|byte| byte.is_ascii_digit())
}