Skip to main content

tablo_core/declaration/
segment.rs

1//! Which strings can be a literal URL segment, such as an action's `NAME`.
2
3use std::fmt;
4
5/// Why a path segment cannot be a literal URL segment.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum SegmentFault {
9    /// The segment is empty.
10    Empty,
11    /// The segment is `.` or `..`.
12    Dot,
13    /// The segment holds a character a route segment cannot: whitespace, a control character, a
14    /// double quote, a backslash or one of `/ ? # % & = { } ( )`.
15    Char(char),
16}
17
18impl fmt::Display for SegmentFault {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Empty => f.write_str("is empty"),
22            Self::Dot => f.write_str("is '.' or '..'"),
23            Self::Char(c) => write!(
24                f,
25                "contains {c:?}: quotes, backslashes, control characters, whitespace, URL \
26                 punctuation and the route pattern characters '{{', '}}', '(' and ')' are refused"
27            ),
28        }
29    }
30}
31
32/// Why `segment` cannot be a literal URL segment, if it cannot.
33///
34/// A `const fn`, so an action's `NAME` is checked when the panel's code compiles.
35pub(crate) const fn segment_fault(segment: &str) -> Option<SegmentFault> {
36    let bytes = segment.as_bytes();
37    if bytes.is_empty() {
38        return Some(SegmentFault::Empty);
39    }
40    if matches!(bytes, b"." | b"..") {
41        return Some(SegmentFault::Dot);
42    }
43    let mut at = 0;
44    while at < bytes.len() {
45        let (c, width) = decode_char(bytes, at);
46        if c.is_control()
47            || c.is_whitespace()
48            || matches!(
49                c,
50                '"' | '\\' | '/' | '?' | '#' | '%' | '&' | '=' | '{' | '}' | '(' | ')'
51            )
52        {
53            return Some(SegmentFault::Char(c));
54        }
55        at += width;
56    }
57    None
58}
59
60/// The character starting at byte `at` of valid UTF-8, and its width.
61const fn decode_char(bytes: &[u8], at: usize) -> (char, usize) {
62    let lead = bytes[at] as u32;
63    let (code, width) = if lead < 0x80 {
64        (lead, 1)
65    } else if lead < 0xE0 {
66        ((lead & 0x1F) << 6 | tail(bytes, at + 1), 2)
67    } else if lead < 0xF0 {
68        (
69            (lead & 0x0F) << 12 | tail(bytes, at + 1) << 6 | tail(bytes, at + 2),
70            3,
71        )
72    } else {
73        (
74            (lead & 0x07) << 18
75                | tail(bytes, at + 1) << 12
76                | tail(bytes, at + 2) << 6
77                | tail(bytes, at + 3),
78            4,
79        )
80    };
81    match char::from_u32(code) {
82        Some(c) => (c, width),
83        None => panic!("a str holds valid UTF-8"),
84    }
85}
86
87/// The six payload bits of the continuation byte at `at`.
88const fn tail(bytes: &[u8], at: usize) -> u32 {
89    bytes[at] as u32 & 0x3F
90}