busylib 0.0.2

BUSY Bar Rust HTTP client
Documentation
//! Validation rules shared by the value types

pub fn name(value: &str) -> bool {
    !value.is_empty() && value.chars().all(is_name_char)
}

pub fn asset_path(value: &str) -> bool {
    !value.is_empty()
        && value
            .chars()
            .all(|character| is_name_char(character) || character == '/')
}

pub fn stock_path(value: &str) -> bool {
    match value.strip_prefix("shared/") {
        Some(rest) => {
            !rest.is_empty()
                && rest.chars().all(|character| {
                    character.is_ascii_lowercase()
                        || character.is_ascii_digit()
                        || matches!(character, '_' | '.')
                })
        }
        None => false,
    }
}

pub fn storage_path(value: &str) -> bool {
    let Some(rest) = value.strip_prefix("/ext") else {
        return false;
    };

    if rest.is_empty() {
        return true;
    }

    if !rest.starts_with('/') {
        return false;
    }

    rest.split('/')
        .skip(1)
        .all(|segment| segment.chars().all(is_name_char))
}

pub fn device_name(value: &str) -> bool {
    let length = value.chars().count();
    (1..=20).contains(&length)
        && value.chars().all(|character| {
            character.is_ascii_alphanumeric()
                || matches!(
                    character,
                    ' ' | '!'
                        | '('
                        | ')'
                        | '_'
                        | '='
                        | '+'
                        | ';'
                        | ':'
                        | ','
                        | '.'
                        | '?'
                        | '\''
                        | '|'
                        | '@'
                        | '#'
                        | '$'
                        | '%'
                        | '^'
                        | '&'
                        | '*'
                        | '['
                        | ']'
                        | '{'
                        | '}'
                        | '/'
                        | '\\'
                        | '"'
                        | '<'
                        | '>'
                        | '-'
                )
        })
}

pub fn printable_ascii(value: &str) -> bool {
    !value.is_empty() && value.bytes().all(|byte| (0x20..=0x7e).contains(&byte))
}

pub fn log_name(value: &str) -> bool {
    !value.is_empty()
        && value
            .chars()
            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
}

pub fn timezone_name(value: &str) -> bool {
    let mut characters = value.chars();
    let Some(first) = characters.next() else {
        return false;
    };
    first.is_ascii_alphabetic()
        && value.chars().count() <= 51
        && characters.all(|character| {
            character.is_ascii_alphanumeric() || matches!(character, ' ' | '_' | '+' | '-')
        })
}

pub fn time_of_day(value: &str) -> bool {
    let bytes = value.as_bytes();
    if bytes.len() != 5 || bytes[2] != b':' {
        return false;
    }
    matches!(
        (two_digits(&bytes[..2]), two_digits(&bytes[3..])),
        (Some(hours), Some(minutes)) if hours <= 23 && minutes <= 59
    )
}

pub fn access_key(value: &str) -> bool {
    (4..=10).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_digit())
}

pub fn timestamp(value: &str) -> bool {
    let bytes = value.as_bytes();

    if bytes.len() < 20 || &bytes[..2] != b"20" {
        return false;
    }

    if two_digits(&bytes[2..4]).is_none() {
        return false;
    }

    if bytes[4] != b'-'
        || bytes[7] != b'-'
        || bytes[10] != b'T'
        || bytes[13] != b':'
        || bytes[16] != b':'
    {
        return false;
    }

    let (Some(month), Some(day), Some(hour), Some(minute), Some(second)) = (
        two_digits(&bytes[5..7]),
        two_digits(&bytes[8..10]),
        two_digits(&bytes[11..13]),
        two_digits(&bytes[14..16]),
        two_digits(&bytes[17..19]),
    ) else {
        return false;
    };

    if !(1..=12).contains(&month)
        || !(1..=31).contains(&day)
        || hour > 23
        || minute > 59
        || second > 59
    {
        return false;
    }

    offset(&bytes[19..])
}

fn offset(bytes: &[u8]) -> bool {
    if bytes == b"Z" {
        return true;
    }

    if !matches!(bytes.first(), Some(b'+' | b'-')) || bytes.len() < 3 {
        return false;
    }

    match two_digits(&bytes[1..3]) {
        Some(hours) if hours <= 23 => {}
        _ => return false,
    }

    match bytes.len() {
        3 => true,
        5 => two_digits(&bytes[3..5]).is_some_and(|minutes| minutes <= 59),
        6 => bytes[3] == b':' && two_digits(&bytes[4..6]).is_some_and(|minutes| minutes <= 59),
        _ => false,
    }
}

fn two_digits(bytes: &[u8]) -> Option<u8> {
    match bytes {
        [tens, ones] if tens.is_ascii_digit() && ones.is_ascii_digit() => {
            Some((tens - b'0') * 10 + (ones - b'0'))
        }
        _ => None,
    }
}

fn is_name_char(character: char) -> bool {
    character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-')
}