1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//! Matrix session ID.

use ruma_macros::IdZst;

use super::IdParseError;

/// A session ID.
///
/// Session IDs in Matrix are opaque character sequences of `[0-9a-zA-Z.=_-]`. Their length must
/// must not exceed 255 characters.
#[repr(transparent)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)]
#[ruma_id(validate = validate_session_id)]
pub struct SessionId(str);

impl SessionId {
    #[doc(hidden)]
    pub const fn _priv_const_new(s: &str) -> Result<&Self, &'static str> {
        match validate_session_id(s) {
            Ok(()) => Ok(Self::from_borrowed(s)),
            Err(IdParseError::MaximumLengthExceeded) => {
                Err("Invalid Session ID: exceeds 255 bytes")
            }
            Err(IdParseError::InvalidCharacters) => {
                Err("Invalid Session ID: contains invalid characters")
            }
            Err(IdParseError::Empty) => Err("Invalid Session ID: empty"),
            Err(_) => unreachable!(),
        }
    }
}

const fn validate_session_id(s: &str) -> Result<(), IdParseError> {
    if s.len() > 255 {
        return Err(IdParseError::MaximumLengthExceeded);
    } else if contains_invalid_byte(s.as_bytes()) {
        return Err(IdParseError::InvalidCharacters);
    } else if s.is_empty() {
        return Err(IdParseError::Empty);
    }

    Ok(())
}

const fn contains_invalid_byte(mut bytes: &[u8]) -> bool {
    // non-const form:
    //
    // bytes.iter().all(|b| b.is_ascii_alphanumeric() || b".=_-".contains(&b))
    loop {
        if let Some((byte, rest)) = bytes.split_first() {
            if byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'=' | b'_' | b'-') {
                bytes = rest;
            } else {
                break true;
            }
        } else {
            break false;
        }
    }
}