Skip to main content

sark_http/h1/
switch.rs

1//! HTTP/1 protocol-transition admission.
2
3use core::mem;
4
5use crate::{
6    field::{self, scan::Match},
7    h1::{
8        field::Connection,
9        head::{line::Version, request::offers},
10    },
11    payload::response::Method,
12};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Error {
16    IncompleteUpgradeRequest,
17    UnexpectedSwitch,
18    MissingConnectionUpgrade,
19    MissingSelection,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum UpgradeError {
24    Empty,
25    InvalidProtocol,
26    MultipleSelections,
27    NotOffered,
28    Overflow { value: usize, added: usize },
29    TooLarge { actual: usize, maximum: usize },
30}
31
32/// Borrow-free transition requested by an admitted HTTP/1 exchange.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34#[repr(u8)]
35pub enum Intent {
36    #[default]
37    None,
38    Connect,
39    Upgrade,
40}
41
42impl Intent {
43    /// Derives the only transition intent which may cross the request-head
44    /// boundary. HTTP/1.0 ignores Upgrade; HTTP/1.1 rejects a partial handshake.
45    pub(in crate::h1) const fn from_request(
46        request: Method,
47        conn: Connection,
48        offers: Offers,
49        version: Version,
50    ) -> Result<Self, Error> {
51        if matches!(request, Method::Connect) {
52            return Ok(Self::Connect);
53        }
54        match (conn.is_upgrade(), offers.is_present()) {
55            (false, false) => Ok(Self::None),
56            _ if matches!(version, Version::Ten) => Ok(Self::None),
57            (true, true) => Ok(Self::Upgrade),
58            (true, false) | (false, true) => Err(Error::IncompleteUpgradeRequest),
59        }
60    }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(u8)]
65pub enum Kind {
66    Connect,
67    Upgrade,
68    ExtendedConnect,
69}
70
71/// Proof that one response Upgrade selection is syntactically valid and was
72/// present in the request's offer set. Construction performs exact
73/// case-insensitive byte comparison; no collision-prone digest is retained.
74#[derive(Clone, Copy, PartialEq, Eq)]
75pub struct Selection {
76    private: (),
77}
78
79impl Selection {
80    pub(in crate::h1) fn from_offers<B>(
81        offered: offers::View<'_, B>,
82        selected: &[u8],
83    ) -> Result<Self, UpgradeError> {
84        let selected = single_upgrade_protocol(selected)?;
85        for value in offered.values() {
86            for protocol in value.split(|byte| *byte == b',') {
87                if field::Error::trim_ows(protocol).eq_ignore_ascii_case(selected) {
88                    return Ok(Self { private: () });
89                }
90            }
91        }
92        Err(UpgradeError::NotOffered)
93    }
94
95    pub fn select<'a>(
96        offered: impl IntoIterator<Item = &'a [u8]>,
97        selected: &[u8],
98    ) -> Result<Self, UpgradeError> {
99        let selected = single_upgrade_protocol(selected)?;
100        let mut matched = false;
101        for value in offered {
102            for protocol in protocols(value) {
103                let protocol = protocol?;
104                if protocol.eq_ignore_ascii_case(selected) {
105                    matched = true;
106                }
107            }
108            if matched {
109                return Ok(Self { private: () });
110            }
111        }
112        Err(UpgradeError::NotOffered)
113    }
114}
115
116/// Exact, borrow-free retention plan accumulated during the head scan.
117#[repr(transparent)]
118#[derive(Clone, Copy, Default, PartialEq, Eq)]
119pub struct Offers(u32);
120
121impl Offers {
122    /// Admits one `Upgrade` field and accounts for the NUL separator used by a
123    /// retained offer set. Validation and exact-length planning therefore
124    /// happen in the canonical request-head walk.
125    pub(in crate::h1) fn try_push(self, value: &[u8]) -> Result<Self, UpgradeError> {
126        Self::validate(value)?;
127        let sep = usize::from(self.is_present());
128        let current = self.compact_len();
129        let length = current.checked_add(sep).ok_or(UpgradeError::Overflow {
130            value: current,
131            added: sep,
132        })?;
133        let actual = length
134            .checked_add(value.len())
135            .ok_or(UpgradeError::Overflow {
136                value: length,
137                added: value.len(),
138            })?;
139        let maximum = u32::MAX as usize;
140        if actual > maximum {
141            return Err(UpgradeError::TooLarge { actual, maximum });
142        }
143        Ok(Self(actual as u32))
144    }
145
146    pub const fn is_present(self) -> bool {
147        self.0 != 0
148    }
149
150    pub const fn compact_len(self) -> usize {
151        self.0 as usize
152    }
153
154    pub fn validate(value: &[u8]) -> Result<(), UpgradeError> {
155        for raw in value.split(|byte| *byte == b',') {
156            check(field::Error::trim_ows(raw))?;
157        }
158        Ok(())
159    }
160}
161
162fn protocols(value: &[u8]) -> impl Iterator<Item = Result<&[u8], UpgradeError>> {
163    value.split(|byte| *byte == b',').map(|raw| {
164        let protocol = field::Error::trim_ows(raw);
165        check(protocol)?;
166        Ok(protocol)
167    })
168}
169
170fn check(protocol: &[u8]) -> Result<(), UpgradeError> {
171    if protocol.is_empty() {
172        Err(UpgradeError::Empty)
173    } else if is_protocol(protocol) {
174        Ok(())
175    } else {
176        Err(UpgradeError::InvalidProtocol)
177    }
178}
179
180fn single_upgrade_protocol(value: &[u8]) -> Result<&[u8], UpgradeError> {
181    let value = field::Error::trim_ows(value);
182    if value.is_empty() {
183        return Err(UpgradeError::Empty);
184    }
185    if value.contains(&b',') {
186        return Err(UpgradeError::MultipleSelections);
187    }
188    if !is_protocol(value) {
189        return Err(UpgradeError::InvalidProtocol);
190    }
191    Ok(value)
192}
193
194fn is_protocol(value: &[u8]) -> bool {
195    let Some(slash) = Match::find(b'/', value) else {
196        return is_token(value);
197    };
198    is_token(slash.prefix()) && is_token(slash.suffix())
199}
200
201fn is_token(value: &[u8]) -> bool {
202    !value.is_empty() && value.iter().copied().all(field::Error::is_name_byte)
203}
204
205const _: () = assert!(mem::size_of::<UpgradeError>() <= 3 * mem::size_of::<usize>());
206const _: () = assert!(!mem::needs_drop::<UpgradeError>());
207const _: () = assert!(core::mem::size_of::<Intent>() == 1);
208const _: () = assert!(core::mem::size_of::<Offers>() == 4);