sozu_lib/protocol/kawa_h1/
parser.rs1use std::{
9 cmp::min,
10 fmt::{self, Write},
11 ops::Deref,
12};
13
14use nom::{
15 AsChar, Err, IResult, Parser,
16 bytes::{self, complete::take_while},
17 character::complete::digit1,
18 combinator::opt,
19 error::{Error, ErrorKind},
20 sequence::preceded,
21};
22
23pub fn compare_no_case(left: &[u8], right: &[u8]) -> bool {
24 if left.len() != right.len() {
25 return false;
26 }
27
28 left.iter().zip(right).all(|(a, b)| match (*a, *b) {
29 (0..=64, 0..=64) | (91..=96, 91..=96) | (123..=255, 123..=255) => a == b,
30 (65..=90, 65..=90) | (97..=122, 97..=122) | (65..=90, 97..=122) | (97..=122, 65..=90) => {
31 *a | 0b00_10_00_00 == *b | 0b00_10_00_00
32 }
33 _ => false,
34 })
35}
36
37#[derive(PartialEq, Eq, Clone)]
38pub enum Method {
39 Get,
40 Post,
41 Head,
42 Options,
43 Put,
44 Delete,
45 Trace,
46 Connect,
47 Custom(String),
48}
49
50impl fmt::Debug for Method {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Get => f.write_str("Get"),
54 Self::Post => f.write_str("Post"),
55 Self::Head => f.write_str("Head"),
56 Self::Options => f.write_str("Options"),
57 Self::Put => f.write_str("Put"),
58 Self::Delete => f.write_str("Delete"),
59 Self::Trace => f.write_str("Trace"),
60 Self::Connect => f.write_str("Connect"),
61 Self::Custom(custom) => write!(f, "Custom(bytes={})", custom.len()),
62 }
63 }
64}
65
66impl Method {
67 pub fn new(s: &[u8]) -> Method {
76 match s {
77 b"GET" => Method::Get,
78 b"POST" => Method::Post,
79 b"HEAD" => Method::Head,
80 b"OPTIONS" => Method::Options,
81 b"PUT" => Method::Put,
82 b"DELETE" => Method::Delete,
83 b"TRACE" => Method::Trace,
84 b"CONNECT" => Method::Connect,
85 _ => Method::Custom(String::from_utf8_lossy(s).into_owned()),
86 }
87 }
88
89 pub fn is_idempotent(&self) -> bool {
114 match self {
115 Method::Get
116 | Method::Head
117 | Method::Put
118 | Method::Delete
119 | Method::Options
120 | Method::Trace => true,
121 Method::Post | Method::Connect | Method::Custom(_) => false,
122 }
123 }
124}
125
126impl AsRef<str> for Method {
127 fn as_ref(&self) -> &str {
128 match self {
129 Self::Get => "GET",
130 Self::Post => "POST",
131 Self::Head => "HEAD",
132 Self::Options => "OPTIONS",
133 Self::Put => "PUT",
134 Self::Delete => "DELETE",
135 Self::Trace => "TRACE",
136 Self::Connect => "CONNECT",
137 Self::Custom(custom) => custom,
138 }
139 }
140}
141
142impl Deref for Method {
143 type Target = str;
144
145 fn deref(&self) -> &Self::Target {
146 self.as_ref()
147 }
148}
149
150impl fmt::Display for Method {
151 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152 write!(f, "{}", self.as_ref())
153 }
154}
155
156#[cfg(feature = "tolerant-http1-parser")]
157fn is_hostname_char(i: u8) -> bool {
158 i.is_alphanum() ||
159 b"-._".contains(&i)
169}
170
171#[cfg(not(feature = "tolerant-http1-parser"))]
172fn is_hostname_char(i: u8) -> bool {
173 i.is_alphanum() ||
174 b"-.".contains(&i)
180}
181
182pub fn hostname_and_port(i: &[u8]) -> IResult<&[u8], (&[u8], Option<u16>)> {
183 let (i, host) = take_while(is_hostname_char)(i)?;
184 if host.is_empty() {
185 return Err(Err::Error(Error::new(i, ErrorKind::Alpha)));
186 }
187 let (i, port_bytes) = opt(preceded(bytes::complete::tag(":"), digit1)).parse(i)?;
188 let port = match port_bytes {
189 Some(bytes) => match std::str::from_utf8(bytes).unwrap().parse::<u16>() {
194 Ok(p) if p != 0 => Some(p),
195 _ => return Err(Err::Error(Error::new(bytes, ErrorKind::Digit))),
196 },
197 None => None,
198 };
199
200 if !i.is_empty() {
201 return Err(Err::Error(Error::new(i, ErrorKind::Eof)));
202 }
203 Ok((i, (host, port)))
204}
205
206pub fn view(buf: &[u8], size: usize, points: &[usize]) -> String {
207 let mut view = format!("{points:?} => ");
208 let mut end = 0;
209 for (i, point) in points.iter().enumerate() {
210 if *point > buf.len() {
211 break;
212 }
213 let start = if end + size < *point {
214 view.push_str("... ");
215 point - size
216 } else {
217 end
218 };
219 let stop = if i + 1 < points.len() {
220 min(buf.len(), points[i + 1])
221 } else {
222 buf.len()
223 };
224 end = if point + size > stop {
225 stop
226 } else {
227 point + size
228 };
229 for element in &buf[start..*point] {
230 let _ = view.write_fmt(format_args!("{element:02X} "));
231 }
232 view.push_str("| ");
233 for element in &buf[*point..end] {
234 let _ = view.write_fmt(format_args!("{element:02X} "));
235 }
236 }
237 if end < buf.len() {
238 view.push_str("...")
239 }
240 view
241}
242
243#[test]
244fn test_view_out_of_bound() {
245 println!(
246 "{}",
247 view(
248 &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
249 2,
250 &[5, 5, 8, 9, 9, 13, 80]
251 )
252 );
253}
254
255#[test]
256fn custom_method_does_not_assume_utf8() {
257 let method = Method::new(b"\xFFBAD");
258
259 assert_eq!(method, Method::Custom("\u{FFFD}BAD".to_owned()));
260}
261
262#[test]
263fn custom_method_debug_is_bounded() {
264 const METHOD_SECRET: &str = "CUSTOM_METHOD_DEBUG_SECRET_SENTINEL";
265
266 let custom = format!("{METHOD_SECRET}{}", "x".repeat(4096));
267 let custom_len = custom.len();
268 let output = format!("{:?}", Method::Custom(custom));
269
270 assert!(
271 !output.contains(METHOD_SECRET),
272 "custom Method Debug leaked its payload: {output}"
273 );
274 assert!(
275 output.contains(&format!("bytes={custom_len}")),
276 "custom Method Debug omitted its byte length: {output}"
277 );
278 assert!(
279 output.len() <= 128,
280 "custom Method Debug is not bounded: {} bytes",
281 output.len()
282 );
283}
284
285#[test]
286fn hostname_and_port_rejects_empty_host() {
287 assert!(hostname_and_port(b":80").is_err());
288}
289
290#[test]
291fn hostname_and_port_rejects_out_of_range_port() {
292 assert!(hostname_and_port(b"example.com:65536").is_err());
293}
294
295#[test]
296fn hostname_and_port_rejects_port_zero() {
297 assert!(hostname_and_port(b"example.com:0").is_err());
299}
300
301#[test]
302fn hostname_and_port_accepts_u16_port() {
303 let (remaining, (host, port)) = hostname_and_port(b"example.com:65535").unwrap();
304
305 assert!(remaining.is_empty());
306 assert_eq!(host, b"example.com");
307 assert_eq!(port, Some(65535));
308}
309
310#[test]
311fn hostname_and_port_returns_no_port_when_absent() {
312 let (remaining, (host, port)) = hostname_and_port(b"example.com").unwrap();
313
314 assert!(remaining.is_empty());
315 assert_eq!(host, b"example.com");
316 assert_eq!(port, None);
317}
318
319#[test]
340fn is_hostname_char_admits_only_ascii() {
341 for byte in u8::MIN..=u8::MAX {
342 assert!(
343 !is_hostname_char(byte) || byte.is_ascii(),
344 "is_hostname_char accepted the non-ASCII byte {byte:#04x}; \
345 DomainRule::from_str's Unicode case folding stops being \
346 unobservable the moment one of these can reach a hostname",
347 );
348 }
349
350 assert!(
353 hostname_and_port("café.example.com".as_bytes()).is_err(),
354 "a non-ASCII byte must fail the whole authority, not truncate it to `caf`",
355 );
356
357 let (remaining, (host, port)) =
360 hostname_and_port(b"cafe.example.com").expect("the ASCII spelling must parse");
361 assert!(remaining.is_empty());
362 assert_eq!(host, b"cafe.example.com");
363 assert_eq!(port, None);
364}
365
366#[test]
367fn lowercase_verb_is_custom_and_not_idempotent() {
368 let spellings: [&[u8]; 11] = [
374 b"get", b"Get", b"gEt", b"put", b"Put", b"delete", b"head", b"options", b"trace", b"post",
375 b"Post",
376 ];
377
378 for raw in spellings {
379 let method = Method::new(raw);
380 let spelling = String::from_utf8_lossy(raw).into_owned();
381
382 assert_eq!(
383 method,
384 Method::Custom(spelling.clone()),
385 "{spelling} must parse as a custom method, not as a known verb"
386 );
387 assert!(
388 !method.is_idempotent(),
389 "{spelling} must not be judged idempotent"
390 );
391 }
392}
393
394#[test]
395fn canonical_verbs_keep_their_variant() {
396 assert_eq!(Method::new(b"GET"), Method::Get);
397 assert_eq!(Method::new(b"POST"), Method::Post);
398 assert_eq!(Method::new(b"HEAD"), Method::Head);
399 assert_eq!(Method::new(b"OPTIONS"), Method::Options);
400 assert_eq!(Method::new(b"PUT"), Method::Put);
401 assert_eq!(Method::new(b"DELETE"), Method::Delete);
402 assert_eq!(Method::new(b"TRACE"), Method::Trace);
403 assert_eq!(Method::new(b"CONNECT"), Method::Connect);
404}