1use core::fmt;
4
5mod validation;
6
7use validation::{validate_path, validate_query};
8
9pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum RequestPathError {
15 Empty,
17 NotOriginForm,
19 TooLong,
21 InvalidByte,
23 DoubledSlash,
25 DotSegment,
27 InvalidPercentTriplet,
29 LowercasePercentHex,
31 EncodedSeparator,
33 EncodedControl,
35 EncodedUnreserved,
37}
38
39impl_static_error!(RequestPathError,
40 Self::Empty => "request path is empty",
41 Self::NotOriginForm => "request path is not in origin form",
42 Self::TooLong => "request path exceeds the length limit",
43 Self::InvalidByte => "request path contains a forbidden byte",
44 Self::DoubledSlash => "request path contains an empty segment",
45 Self::DotSegment => "request path contains a dot segment",
46 Self::InvalidPercentTriplet => "request path contains malformed percent encoding",
47 Self::LowercasePercentHex => "request path percent encoding is not uppercase",
48 Self::EncodedSeparator => "request path percent encoding hides a separator",
49 Self::EncodedControl => "request path percent encoding hides a control byte",
50 Self::EncodedUnreserved => "request path percent encodes an unreserved byte",
51);
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum StructuredQueryError {
56 TooLong,
58 InvalidByte,
60 EmptyPair,
62 EmptyKey,
64 MultipleEquals,
66 InvalidPercentTriplet,
68 LowercasePercentHex,
70 EncodedControl,
72 EncodedUnreserved,
74 PlusForbidden,
76}
77
78impl_static_error!(StructuredQueryError,
79 Self::TooLong => "query exceeds the length limit",
80 Self::InvalidByte => "query contains a forbidden byte",
81 Self::EmptyPair => "query contains an empty pair",
82 Self::EmptyKey => "query key is empty",
83 Self::MultipleEquals => "query pair contains multiple separators",
84 Self::InvalidPercentTriplet => "query contains malformed percent encoding",
85 Self::LowercasePercentHex => "query percent encoding is not uppercase",
86 Self::EncodedControl => "query percent encoding hides a control or fragment byte",
87 Self::EncodedUnreserved => "query percent encodes an unreserved byte",
88 Self::PlusForbidden => "query uses form-style plus encoding",
89);
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum RequestTargetError {
94 Path(RequestPathError),
96 Query(StructuredQueryError),
98 TooLong,
100 OutputTooSmall,
102}
103
104impl fmt::Display for RequestTargetError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Path(error) => write!(formatter, "invalid request path: {error}"),
108 Self::Query(error) => write!(formatter, "invalid request query: {error}"),
109 Self::TooLong => formatter.write_str("request target exceeds the length limit"),
110 Self::OutputTooSmall => formatter.write_str("request target output is too small"),
111 }
112 }
113}
114
115impl core::error::Error for RequestTargetError {}
116
117#[derive(Clone, Copy, Eq, PartialEq)]
119pub struct RequestPath<'a>(&'a str);
120
121impl<'a> RequestPath<'a> {
122 pub fn new(value: &'a str) -> Result<Self, RequestPathError> {
124 validate_path(value)?;
125 Ok(Self(value))
126 }
127
128 #[must_use]
130 pub const fn as_str(self) -> &'a str {
131 self.0
132 }
133}
134
135impl fmt::Debug for RequestPath<'_> {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str("RequestPath([redacted])")
138 }
139}
140
141#[derive(Clone, Copy, Eq, PartialEq)]
143pub struct CanonicalQuery<'a>(&'a str);
144
145impl<'a> CanonicalQuery<'a> {
146 pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
148 validate_query(value, false)?;
149 Ok(Self(value))
150 }
151
152 #[must_use]
154 pub const fn as_str(self) -> &'a str {
155 self.0
156 }
157
158 #[must_use]
160 pub const fn pairs(self) -> QueryPairs<'a> {
161 QueryPairs::new(self.0)
162 }
163}
164
165impl fmt::Debug for CanonicalQuery<'_> {
166 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 formatter.write_str("CanonicalQuery([redacted])")
168 }
169}
170
171#[derive(Clone, Copy, Eq, PartialEq)]
173pub struct FormQuery<'a>(&'a str);
174
175impl<'a> FormQuery<'a> {
176 pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
178 validate_query(value, true)?;
179 Ok(Self(value))
180 }
181
182 #[must_use]
184 pub const fn as_str(self) -> &'a str {
185 self.0
186 }
187
188 #[must_use]
190 pub const fn pairs(self) -> QueryPairs<'a> {
191 QueryPairs::new(self.0)
192 }
193}
194
195impl fmt::Debug for FormQuery<'_> {
196 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197 formatter.write_str("FormQuery([redacted])")
198 }
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum RequestQuery<'a> {
204 Absent,
206 Canonical(CanonicalQuery<'a>),
208 Form(FormQuery<'a>),
210}
211
212impl<'a> RequestQuery<'a> {
213 #[must_use]
215 pub const fn is_present(self) -> bool {
216 !matches!(self, Self::Absent)
217 }
218
219 #[must_use]
221 pub const fn as_str(self) -> Option<&'a str> {
222 match self {
223 Self::Absent => None,
224 Self::Canonical(query) => Some(query.as_str()),
225 Self::Form(query) => Some(query.as_str()),
226 }
227 }
228}
229
230#[derive(Clone, Copy, Eq, PartialEq)]
232pub struct QueryPair<'a> {
233 key: &'a str,
234 value: Option<&'a str>,
235}
236
237impl<'a> QueryPair<'a> {
238 #[must_use]
240 pub const fn key(self) -> &'a str {
241 self.key
242 }
243
244 #[must_use]
246 pub const fn value(self) -> Option<&'a str> {
247 self.value
248 }
249}
250
251impl fmt::Debug for QueryPair<'_> {
252 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
253 formatter.write_str("QueryPair([redacted])")
254 }
255}
256
257#[derive(Clone)]
259pub struct QueryPairs<'a> {
260 remaining: &'a str,
261}
262
263impl<'a> QueryPairs<'a> {
264 const fn new(value: &'a str) -> Self {
265 Self { remaining: value }
266 }
267}
268
269impl fmt::Debug for QueryPairs<'_> {
270 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271 formatter.write_str("QueryPairs([redacted])")
272 }
273}
274
275impl<'a> Iterator for QueryPairs<'a> {
276 type Item = QueryPair<'a>;
277
278 fn next(&mut self) -> Option<Self::Item> {
279 if self.remaining.is_empty() {
280 return None;
281 }
282 let (pair, remaining) = match self.remaining.split_once('&') {
283 Some(parts) => parts,
284 None => (self.remaining, ""),
285 };
286 self.remaining = remaining;
287 let (key, value) = match pair.split_once('=') {
288 Some((key, value)) => (key, Some(value)),
289 None => (pair, None),
290 };
291 Some(QueryPair { key, value })
292 }
293}
294
295#[derive(Clone, Copy, Eq, PartialEq)]
297pub struct RequestTarget<'a> {
298 value: &'a str,
299 path: RequestPath<'a>,
300 query: RequestQuery<'a>,
301}
302
303impl<'a> RequestTarget<'a> {
304 pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
309 if value.len() > MAX_REQUEST_TARGET_BYTES {
310 return Err(RequestTargetError::TooLong);
311 }
312 let (path, query) = match value.split_once('?') {
313 Some((path, query)) => (
314 RequestPath::new(path).map_err(RequestTargetError::Path)?,
315 RequestQuery::Canonical(
316 CanonicalQuery::new(query).map_err(RequestTargetError::Query)?,
317 ),
318 ),
319 None => (
320 RequestPath::new(value).map_err(RequestTargetError::Path)?,
321 RequestQuery::Absent,
322 ),
323 };
324 Ok(Self { value, path, query })
325 }
326
327 pub fn assemble<'output>(
336 path: RequestPath<'_>,
337 query: RequestQuery<'_>,
338 output: &'output mut [u8],
339 ) -> Result<RequestTarget<'output>, RequestTargetError> {
340 let query_len = query.as_borrowed_str().map_or(0, str::len);
341 let delimiter_len = usize::from(query.is_present());
342 let len = path
343 .as_str()
344 .len()
345 .checked_add(delimiter_len)
346 .and_then(|len| len.checked_add(query_len))
347 .ok_or(RequestTargetError::TooLong)?;
348 if len > MAX_REQUEST_TARGET_BYTES {
349 return Err(RequestTargetError::TooLong);
350 }
351 let target = output
352 .get_mut(..len)
353 .ok_or(RequestTargetError::OutputTooSmall)?;
354 let path_len = path.as_str().len();
355 let (path_output, suffix) = target.split_at_mut(path_len);
356 path_output.copy_from_slice(path.as_str().as_bytes());
357 if query.is_present() {
358 let (delimiter, query_output) = suffix.split_at_mut(1);
359 let delimiter = delimiter
360 .first_mut()
361 .ok_or(RequestTargetError::OutputTooSmall)?;
362 *delimiter = b'?';
363 if let Some(query) = query.as_borrowed_str() {
364 query_output.copy_from_slice(query.as_bytes());
365 }
366 }
367 let value = core::str::from_utf8(target).map_err(|_| RequestTargetError::OutputTooSmall)?;
368 let path_value = value
369 .get(..path_len)
370 .ok_or(RequestTargetError::OutputTooSmall)?;
371 let path = RequestPath(path_value);
372 let query = match query {
373 RequestQuery::Absent => RequestQuery::Absent,
374 RequestQuery::Canonical(_) => {
375 let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
376 let query_value = value
377 .get(query_start..)
378 .ok_or(RequestTargetError::OutputTooSmall)?;
379 RequestQuery::Canonical(CanonicalQuery(query_value))
380 }
381 RequestQuery::Form(_) => {
382 let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
383 let query_value = value
384 .get(query_start..)
385 .ok_or(RequestTargetError::OutputTooSmall)?;
386 RequestQuery::Form(FormQuery(query_value))
387 }
388 };
389 Ok(RequestTarget { value, path, query })
390 }
391
392 #[must_use]
394 pub const fn as_str(self) -> &'a str {
395 self.value
396 }
397
398 #[must_use]
400 pub const fn path(self) -> RequestPath<'a> {
401 self.path
402 }
403
404 #[must_use]
406 pub const fn query(self) -> RequestQuery<'a> {
407 self.query
408 }
409
410 #[must_use]
412 pub fn query_bytes(self) -> Option<&'a [u8]> {
413 self.query().as_borrowed_str().map(str::as_bytes)
414 }
415
416 #[must_use]
418 pub const fn len(self) -> usize {
419 self.value.len()
420 }
421
422 #[must_use]
424 pub const fn is_empty(self) -> bool {
425 false
426 }
427}
428
429impl fmt::Debug for RequestTarget<'_> {
430 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
431 formatter.write_str("RequestTarget([redacted])")
432 }
433}
434
435impl<'a> RequestQuery<'a> {
436 const fn as_borrowed_str(self) -> Option<&'a str> {
437 match self {
438 Self::Absent => None,
439 Self::Canonical(query) => Some(query.as_str()),
440 Self::Form(query) => Some(query.as_str()),
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests;