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