rama_http_core/ext/
h1_reason_phrase.rs

1use bytes::Bytes;
2
3/// A reason phrase in an HTTP/1 response.
4///
5/// # Clients
6///
7/// For clients, a `ReasonPhrase` will be present in the extensions of the `http::Response` returned
8/// for a request if the reason phrase is different from the canonical reason phrase for the
9/// response's status code. For example, if a server returns `HTTP/1.1 200 Awesome`, the
10/// `ReasonPhrase` will be present and contain `Awesome`, but if a server returns `HTTP/1.1 200 OK`,
11/// the response will not contain a `ReasonPhrase`.
12///
13/// # Servers
14///
15/// When a `ReasonPhrase` is present in the extensions of the `http::Response` written by a server,
16/// its contents will be written in place of the canonical reason phrase when responding via HTTP/1.
17#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct ReasonPhrase(Bytes);
19
20impl ReasonPhrase {
21    /// Gets the reason phrase as bytes.
22    pub fn as_bytes(&self) -> &[u8] {
23        &self.0
24    }
25
26    /// Converts a static byte slice to a reason phrase.
27    pub const fn from_static(reason: &'static [u8]) -> Self {
28        // TODO: this can be made const once MSRV is >= 1.57.0
29        if find_invalid_byte(reason).is_some() {
30            panic!("invalid byte in static reason phrase");
31        }
32        Self(Bytes::from_static(reason))
33    }
34
35    // Not public on purpose.
36    /// Converts a `Bytes` directly into a `ReasonPhrase` without validating.
37    ///
38    /// Use with care; invalid bytes in a reason phrase can cause serious security problems if
39    /// emitted in a response.
40    pub(crate) fn from_bytes_unchecked(reason: Bytes) -> Self {
41        Self(reason)
42    }
43}
44
45impl TryFrom<&[u8]> for ReasonPhrase {
46    type Error = InvalidReasonPhrase;
47
48    fn try_from(reason: &[u8]) -> Result<Self, Self::Error> {
49        if let Some(bad_byte) = find_invalid_byte(reason) {
50            Err(InvalidReasonPhrase { bad_byte })
51        } else {
52            Ok(Self(Bytes::copy_from_slice(reason)))
53        }
54    }
55}
56
57impl TryFrom<Vec<u8>> for ReasonPhrase {
58    type Error = InvalidReasonPhrase;
59
60    fn try_from(reason: Vec<u8>) -> Result<Self, Self::Error> {
61        if let Some(bad_byte) = find_invalid_byte(&reason) {
62            Err(InvalidReasonPhrase { bad_byte })
63        } else {
64            Ok(Self(Bytes::from(reason)))
65        }
66    }
67}
68
69impl TryFrom<String> for ReasonPhrase {
70    type Error = InvalidReasonPhrase;
71
72    fn try_from(reason: String) -> Result<Self, Self::Error> {
73        if let Some(bad_byte) = find_invalid_byte(reason.as_bytes()) {
74            Err(InvalidReasonPhrase { bad_byte })
75        } else {
76            Ok(Self(Bytes::from(reason)))
77        }
78    }
79}
80
81impl TryFrom<Bytes> for ReasonPhrase {
82    type Error = InvalidReasonPhrase;
83
84    fn try_from(reason: Bytes) -> Result<Self, Self::Error> {
85        if let Some(bad_byte) = find_invalid_byte(&reason) {
86            Err(InvalidReasonPhrase { bad_byte })
87        } else {
88            Ok(Self(reason))
89        }
90    }
91}
92
93impl From<ReasonPhrase> for Bytes {
94    fn from(reason: ReasonPhrase) -> Self {
95        reason.0
96    }
97}
98
99impl AsRef<[u8]> for ReasonPhrase {
100    fn as_ref(&self) -> &[u8] {
101        &self.0
102    }
103}
104
105/// Error indicating an invalid byte when constructing a `ReasonPhrase`.
106///
107/// See [the spec][spec] for details on allowed bytes.
108///
109/// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
110#[derive(Debug)]
111pub struct InvalidReasonPhrase {
112    bad_byte: u8,
113}
114
115impl std::fmt::Display for InvalidReasonPhrase {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "Invalid byte in reason phrase: {}", self.bad_byte)
118    }
119}
120
121impl std::error::Error for InvalidReasonPhrase {}
122
123const fn is_valid_byte(b: u8) -> bool {
124    // See https://www.rfc-editor.org/rfc/rfc5234.html#appendix-B.1
125    const fn is_vchar(b: u8) -> bool {
126        0x21 <= b && b <= 0x7E
127    }
128
129    // See https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values
130    //
131    // The 0xFF comparison is technically redundant, but it matches the text of the spec more
132    // clearly and will be optimized away.
133    #[allow(unused_comparisons, clippy::absurd_extreme_comparisons)]
134    const fn is_obs_text(b: u8) -> bool {
135        0x80 <= b && b <= 0xFF
136    }
137
138    // See https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
139    b == b'\t' || b == b' ' || is_vchar(b) || is_obs_text(b)
140}
141
142const fn find_invalid_byte(bytes: &[u8]) -> Option<u8> {
143    let mut i = 0;
144    while i < bytes.len() {
145        let b = bytes[i];
146        if !is_valid_byte(b) {
147            return Some(b);
148        }
149        i += 1;
150    }
151    None
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn basic_valid() {
160        const PHRASE: &[u8] = b"OK";
161        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
162        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
163    }
164
165    #[test]
166    fn empty_valid() {
167        const PHRASE: &[u8] = b"";
168        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
169        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
170    }
171
172    #[test]
173    fn obs_text_valid() {
174        const PHRASE: &[u8] = b"hyp\xe9r";
175        assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
176        assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
177    }
178
179    const NEWLINE_PHRASE: &[u8] = b"hyp\ner";
180
181    #[test]
182    #[should_panic]
183    fn newline_invalid_panic() {
184        ReasonPhrase::from_static(NEWLINE_PHRASE);
185    }
186
187    #[test]
188    fn newline_invalid_err() {
189        assert!(ReasonPhrase::try_from(NEWLINE_PHRASE).is_err());
190    }
191
192    const CR_PHRASE: &[u8] = b"hyp\rer";
193
194    #[test]
195    #[should_panic]
196    fn cr_invalid_panic() {
197        ReasonPhrase::from_static(CR_PHRASE);
198    }
199
200    #[test]
201    fn cr_invalid_err() {
202        assert!(ReasonPhrase::try_from(CR_PHRASE).is_err());
203    }
204}