sark-client 0.9.0

Simple Asynchronous Rust webKit - Client
Documentation
use http::{Method, Uri};

use crate::connector::error::Error;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum OriginRelation {
    Same,
    Cross,
}

fn origin_relation(a: &Uri, b: &Uri) -> OriginRelation {
    if a.scheme() == b.scheme() && a.host() == b.host() && a.port_u16() == b.port_u16() {
        OriginRelation::Same
    } else {
        OriginRelation::Cross
    }
}

fn resolve(base: &Uri, location: &str) -> Result<Uri, Error> {
    let location = location
        .split_once('#')
        .map_or(location, |(location, _)| location);
    if location.is_empty() {
        return Ok(base.clone());
    }
    if location
        .get(..7)
        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
        || location
            .get(..8)
            .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
    {
        return location
            .parse()
            .map_err(|error| Error::Http(format!("invalid redirect URL: {error}")));
    }
    if location.starts_with("//") {
        let scheme = base
            .scheme_str()
            .ok_or_else(|| Error::Http("missing scheme in base URI".into()))?;
        return format!("{scheme}:{location}")
            .parse()
            .map_err(|error| Error::Http(format!("invalid redirect URL: {error}")));
    }

    let (raw_path, query) = split_location(location);
    let path = if raw_path.is_empty() {
        base.path().to_owned()
    } else if raw_path.starts_with('/') {
        normalize_path(raw_path)
    } else {
        let base_path = base.path();
        let prefix = match base_path.rfind('/') {
            Some(index) => base_path[..=index].to_owned(),
            None => "/".to_owned(),
        };
        normalize_path(&(prefix + raw_path))
    };
    let target = match query {
        Some(query) => format!("{path}?{query}"),
        None => path,
    };
    let mut parts = base.clone().into_parts();
    parts.path_and_query = Some(
        target
            .parse()
            .map_err(|error| Error::Http(format!("invalid redirect target: {error}")))?,
    );
    Uri::from_parts(parts).map_err(|error| Error::Http(format!("invalid redirect URL: {error}")))
}

fn split_location(location: &str) -> (&str, Option<&str>) {
    match location.split_once('?') {
        Some((path, query)) => (path, Some(query)),
        None => (location, None),
    }
}

fn normalize_path(input: &str) -> String {
    let absolute = input.starts_with('/');
    let trailing = input.ends_with('/') || input.ends_with("/.") || input.ends_with("/..");
    let mut segments = Vec::new();
    for segment in input.split('/') {
        if segment.is_empty() || segment == "." {
            continue;
        }
        if segment == ".." {
            segments.pop();
        } else {
            segments.push(segment);
        }
    }
    let mut output = String::with_capacity(input.len());
    if absolute {
        output.push('/');
    }
    for (index, segment) in segments.into_iter().enumerate() {
        if index != 0 {
            output.push('/');
        }
        output.push_str(segment);
    }
    if output.is_empty() || (trailing && !output.ends_with('/')) {
        output.push('/');
    }
    output
}

pub(super) struct RedirectState {
    remaining: u32,
    base: Uri,
    current: Uri,
    visited: Vec<Uri>,
}

impl RedirectState {
    pub(super) fn new(max_redirects: u32, base: &Uri, first_path: &str) -> Result<Self, Error> {
        let current = resolve(base, first_path)?;
        let mut visited = Vec::with_capacity((max_redirects as usize).min(16) + 1);
        visited.push(current.clone());
        Ok(Self {
            remaining: max_redirects,
            base: base.clone(),
            current,
            visited,
        })
    }

    pub(super) fn path_and_query(&self) -> &str {
        match self.current.path_and_query() {
            Some(pq) => pq.as_str(),
            None => self.current.path(),
        }
    }

    pub(super) fn advance(
        &mut self,
        status: u16,
        location: &str,
        method: &Method,
    ) -> Result<Method, Error> {
        if self.remaining == 0 {
            return Err(Error::Http("redirect limit exceeded".into()));
        }
        let new_uri = resolve(&self.current, location)?;
        if origin_relation(&self.base, &new_uri) == OriginRelation::Cross {
            return Err(Error::Http(format!(
                "cross-host redirect to {new_uri} is not supported"
            )));
        }
        if self.visited.contains(&new_uri) {
            return Err(Error::Http("redirect loop detected".into()));
        }
        self.visited.push(new_uri.clone());
        self.current = new_uri;
        self.remaining -= 1;
        let next_method = if matches!(status, 307 | 308) {
            method.clone()
        } else {
            Method::GET
        };
        Ok(next_method)
    }
}