cloud-sdk 0.35.0

no_std-first provider-neutral cloud SDK foundations.
Documentation
use super::{MAX_REQUEST_TARGET_BYTES, RequestPathError, StructuredQueryError};

pub(super) fn validate_path(value: &str) -> Result<(), RequestPathError> {
    if value.is_empty() {
        return Err(RequestPathError::Empty);
    }
    if value.len() > MAX_REQUEST_TARGET_BYTES {
        return Err(RequestPathError::TooLong);
    }
    if !value.starts_with('/') || value.starts_with("//") {
        return Err(RequestPathError::NotOriginForm);
    }
    if value.contains("//") {
        return Err(RequestPathError::DoubledSlash);
    }
    if value
        .split('/')
        .any(|segment| matches!(segment, "." | ".."))
    {
        return Err(RequestPathError::DotSegment);
    }
    validate_component_bytes(value.as_bytes(), Component::Path, false)
}

pub(super) fn validate_query(value: &str, allow_plus: bool) -> Result<(), StructuredQueryError> {
    if value.len() > MAX_REQUEST_TARGET_BYTES {
        return Err(StructuredQueryError::TooLong);
    }
    if value.is_empty() {
        return Ok(());
    }
    if !allow_plus && value.contains('+') {
        return Err(StructuredQueryError::PlusForbidden);
    }
    for pair in value.split('&') {
        if pair.is_empty() {
            return Err(StructuredQueryError::EmptyPair);
        }
        let (key, value) = match pair.split_once('=') {
            Some(parts) => parts,
            None => (pair, ""),
        };
        if key.is_empty() {
            return Err(StructuredQueryError::EmptyKey);
        }
        if value.contains('=') {
            return Err(StructuredQueryError::MultipleEquals);
        }
    }
    validate_component_bytes(value.as_bytes(), Component::Query, allow_plus)
        .map_err(query_component_error)
}

#[derive(Clone, Copy)]
enum Component {
    Path,
    Query,
}

fn validate_component_bytes(
    bytes: &[u8],
    component: Component,
    allow_plus: bool,
) -> Result<(), RequestPathError> {
    let mut index = 0;
    while index < bytes.len() {
        let byte = *bytes.get(index).ok_or(RequestPathError::InvalidByte)?;
        if byte == b'%' {
            let high_index = index
                .checked_add(1)
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            let low_index = index
                .checked_add(2)
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            let high = *bytes
                .get(high_index)
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            let low = *bytes
                .get(low_index)
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            if decode_hex_case_insensitive(high).is_none()
                || decode_hex_case_insensitive(low).is_none()
            {
                return Err(RequestPathError::InvalidPercentTriplet);
            }
            if high.is_ascii_lowercase() || low.is_ascii_lowercase() {
                return Err(RequestPathError::LowercasePercentHex);
            }
            let decoded = decode_hex(high)
                .and_then(|high| high.checked_mul(16))
                .and_then(|high| decode_hex(low).and_then(|low| high.checked_add(low)))
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            validate_encoded(decoded, component)?;
            index = index
                .checked_add(3)
                .ok_or(RequestPathError::InvalidPercentTriplet)?;
            continue;
        }
        let valid = match component {
            Component::Path => is_path_byte(byte),
            Component::Query => {
                is_unreserved(byte) || matches!(byte, b'&' | b'=') || allow_plus && byte == b'+'
            }
        };
        if !valid {
            return Err(RequestPathError::InvalidByte);
        }
        index = index.checked_add(1).ok_or(RequestPathError::InvalidByte)?;
    }
    Ok(())
}

fn validate_encoded(byte: u8, component: Component) -> Result<(), RequestPathError> {
    if matches!(component, Component::Path) && matches!(byte, b'/' | b'\\' | b'?' | b'%') {
        return Err(RequestPathError::EncodedSeparator);
    }
    if byte < 0x20 || byte == 0x7f || matches!(byte, b'#' | b'\\') {
        return Err(RequestPathError::EncodedControl);
    }
    if is_unreserved(byte) {
        return Err(RequestPathError::EncodedUnreserved);
    }
    Ok(())
}

fn query_component_error(error: RequestPathError) -> StructuredQueryError {
    match error {
        RequestPathError::TooLong => StructuredQueryError::TooLong,
        RequestPathError::InvalidPercentTriplet => StructuredQueryError::InvalidPercentTriplet,
        RequestPathError::LowercasePercentHex => StructuredQueryError::LowercasePercentHex,
        RequestPathError::EncodedControl | RequestPathError::EncodedSeparator => {
            StructuredQueryError::EncodedControl
        }
        RequestPathError::EncodedUnreserved => StructuredQueryError::EncodedUnreserved,
        _ => StructuredQueryError::InvalidByte,
    }
}

const fn is_path_byte(byte: u8) -> bool {
    is_unreserved(byte)
        || matches!(
            byte,
            b'/' | b'!'
                | b'$'
                | b'&'
                | b'\''
                | b'('
                | b')'
                | b'*'
                | b'+'
                | b','
                | b';'
                | b'='
                | b':'
                | b'@'
        )
}

const fn is_unreserved(byte: u8) -> bool {
    matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
}

const fn decode_hex_case_insensitive(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => byte.checked_sub(b'0'),
        b'A'..=b'F' => match byte.checked_sub(b'A') {
            Some(value) => value.checked_add(10),
            None => None,
        },
        b'a'..=b'f' => match byte.checked_sub(b'a') {
            Some(value) => value.checked_add(10),
            None => None,
        },
        _ => None,
    }
}

const fn decode_hex(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => byte.checked_sub(b'0'),
        b'A'..=b'F' => match byte.checked_sub(b'A') {
            Some(value) => value.checked_add(10),
            None => None,
        },
        _ => None,
    }
}