1use core::fmt;
4
5mod provider_link;
6mod validation;
7
8pub use provider_link::ProviderLinkQuery;
9use validation::{validate_path, validate_provider_link_query, validate_query};
10
11pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum RequestPathError {
17 Empty,
19 NotOriginForm,
21 TooLong,
23 InvalidByte,
25 DoubledSlash,
27 DotSegment,
29 InvalidPercentTriplet,
31 LowercasePercentHex,
33 EncodedSeparator,
35 EncodedControl,
37 EncodedUnreserved,
39}
40
41impl_static_error!(RequestPathError,
42 Self::Empty => "request path is empty",
43 Self::NotOriginForm => "request path is not in origin form",
44 Self::TooLong => "request path exceeds the length limit",
45 Self::InvalidByte => "request path contains a forbidden byte",
46 Self::DoubledSlash => "request path contains an empty segment",
47 Self::DotSegment => "request path contains a dot segment",
48 Self::InvalidPercentTriplet => "request path contains malformed percent encoding",
49 Self::LowercasePercentHex => "request path percent encoding is not uppercase",
50 Self::EncodedSeparator => "request path percent encoding hides a separator",
51 Self::EncodedControl => "request path percent encoding hides a control byte",
52 Self::EncodedUnreserved => "request path percent encodes an unreserved byte",
53);
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum StructuredQueryError {
58 TooLong,
60 InvalidByte,
62 EmptyPair,
64 EmptyKey,
66 MultipleEquals,
68 InvalidPercentTriplet,
70 LowercasePercentHex,
72 EncodedControl,
74 EncodedUnreserved,
76 PlusForbidden,
78}
79
80impl_static_error!(StructuredQueryError,
81 Self::TooLong => "query exceeds the length limit",
82 Self::InvalidByte => "query contains a forbidden byte",
83 Self::EmptyPair => "query contains an empty pair",
84 Self::EmptyKey => "query key is empty",
85 Self::MultipleEquals => "query pair contains multiple separators",
86 Self::InvalidPercentTriplet => "query contains malformed percent encoding",
87 Self::LowercasePercentHex => "query percent encoding is not uppercase",
88 Self::EncodedControl => "query percent encoding hides a control or fragment byte",
89 Self::EncodedUnreserved => "query percent encodes an unreserved byte",
90 Self::PlusForbidden => "query uses form-style plus encoding",
91);
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum RequestTargetError {
96 Path(RequestPathError),
98 Query(StructuredQueryError),
100 TooLong,
102 OutputTooSmall,
104 InvalidProviderLinkQuery,
106 ProviderLinkQueryCannotAssemble,
108}
109
110impl fmt::Display for RequestTargetError {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 Self::Path(error) => write!(formatter, "invalid request path: {error}"),
114 Self::Query(error) => write!(formatter, "invalid request query: {error}"),
115 Self::TooLong => formatter.write_str("request target exceeds the length limit"),
116 Self::OutputTooSmall => formatter.write_str("request target output is too small"),
117 Self::InvalidProviderLinkQuery => formatter.write_str("provider link query is invalid"),
118 Self::ProviderLinkQueryCannotAssemble => {
119 formatter.write_str("provider link query cannot be assembled")
120 }
121 }
122 }
123}
124
125impl core::error::Error for RequestTargetError {}
126
127#[derive(Clone, Copy, Eq, PartialEq)]
129pub struct RequestPath<'a>(&'a str);
130
131impl<'a> RequestPath<'a> {
132 pub fn new(value: &'a str) -> Result<Self, RequestPathError> {
134 validate_path(value)?;
135 Ok(Self(value))
136 }
137
138 #[must_use]
140 pub const fn as_str(self) -> &'a str {
141 self.0
142 }
143}
144
145impl fmt::Debug for RequestPath<'_> {
146 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147 formatter.write_str("RequestPath([redacted])")
148 }
149}
150
151#[derive(Clone, Copy, Eq, PartialEq)]
153pub struct CanonicalQuery<'a>(&'a str);
154
155impl<'a> CanonicalQuery<'a> {
156 pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
158 validate_query(value, false)?;
159 Ok(Self(value))
160 }
161
162 #[must_use]
164 pub const fn as_str(self) -> &'a str {
165 self.0
166 }
167
168 #[must_use]
170 pub const fn pairs(self) -> QueryPairs<'a> {
171 QueryPairs::new(self.0)
172 }
173}
174
175impl fmt::Debug for CanonicalQuery<'_> {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 formatter.write_str("CanonicalQuery([redacted])")
178 }
179}
180
181#[derive(Clone, Copy, Eq, PartialEq)]
183pub struct FormQuery<'a>(&'a str);
184
185impl<'a> FormQuery<'a> {
186 pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
188 validate_query(value, true)?;
189 Ok(Self(value))
190 }
191
192 #[must_use]
194 pub const fn as_str(self) -> &'a str {
195 self.0
196 }
197
198 #[must_use]
200 pub const fn pairs(self) -> QueryPairs<'a> {
201 QueryPairs::new(self.0)
202 }
203}
204
205impl fmt::Debug for FormQuery<'_> {
206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207 formatter.write_str("FormQuery([redacted])")
208 }
209}
210
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
213pub enum RequestQuery<'a> {
214 Absent,
216 Canonical(CanonicalQuery<'a>),
218 Form(FormQuery<'a>),
220 ProviderLink(ProviderLinkQuery<'a>),
222}
223
224impl<'a> RequestQuery<'a> {
225 #[must_use]
227 pub const fn is_present(self) -> bool {
228 !matches!(self, Self::Absent)
229 }
230
231 #[must_use]
233 pub const fn as_str(self) -> Option<&'a str> {
234 match self {
235 Self::Absent => None,
236 Self::Canonical(query) => Some(query.as_str()),
237 Self::Form(query) => Some(query.as_str()),
238 Self::ProviderLink(query) => Some(query.as_str()),
239 }
240 }
241}
242
243#[derive(Clone, Copy, Eq, PartialEq)]
245pub struct QueryPair<'a> {
246 key: &'a str,
247 value: Option<&'a str>,
248}
249
250impl<'a> QueryPair<'a> {
251 #[must_use]
253 pub const fn key(self) -> &'a str {
254 self.key
255 }
256
257 #[must_use]
259 pub const fn value(self) -> Option<&'a str> {
260 self.value
261 }
262}
263
264impl fmt::Debug for QueryPair<'_> {
265 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
266 formatter.write_str("QueryPair([redacted])")
267 }
268}
269
270#[derive(Clone)]
272pub struct QueryPairs<'a> {
273 remaining: &'a str,
274}
275
276impl<'a> QueryPairs<'a> {
277 const fn new(value: &'a str) -> Self {
278 Self { remaining: value }
279 }
280}
281
282impl fmt::Debug for QueryPairs<'_> {
283 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284 formatter.write_str("QueryPairs([redacted])")
285 }
286}
287
288impl<'a> Iterator for QueryPairs<'a> {
289 type Item = QueryPair<'a>;
290
291 fn next(&mut self) -> Option<Self::Item> {
292 if self.remaining.is_empty() {
293 return None;
294 }
295 let (pair, remaining) = match self.remaining.split_once('&') {
296 Some(parts) => parts,
297 None => (self.remaining, ""),
298 };
299 self.remaining = remaining;
300 let (key, value) = match pair.split_once('=') {
301 Some((key, value)) => (key, Some(value)),
302 None => (pair, None),
303 };
304 Some(QueryPair { key, value })
305 }
306}
307
308#[derive(Clone, Copy, Eq, PartialEq)]
310pub struct RequestTarget<'a> {
311 value: &'a str,
312 path: RequestPath<'a>,
313 query: RequestQuery<'a>,
314}
315
316impl<'a> RequestTarget<'a> {
317 pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
322 if value.len() > MAX_REQUEST_TARGET_BYTES {
323 return Err(RequestTargetError::TooLong);
324 }
325 let (path, query) = match value.split_once('?') {
326 Some((path, query)) => (
327 RequestPath::new(path).map_err(RequestTargetError::Path)?,
328 RequestQuery::Canonical(
329 CanonicalQuery::new(query).map_err(RequestTargetError::Query)?,
330 ),
331 ),
332 None => (
333 RequestPath::new(value).map_err(RequestTargetError::Path)?,
334 RequestQuery::Absent,
335 ),
336 };
337 Ok(Self { value, path, query })
338 }
339
340 pub fn assemble<'output>(
349 path: RequestPath<'_>,
350 query: RequestQuery<'_>,
351 output: &'output mut [u8],
352 ) -> Result<RequestTarget<'output>, RequestTargetError> {
353 if matches!(query, RequestQuery::ProviderLink(_)) {
354 return Err(RequestTargetError::ProviderLinkQueryCannotAssemble);
355 }
356 let query_len = query.as_borrowed_str().map_or(0, str::len);
357 let delimiter_len = usize::from(query.is_present());
358 let len = path
359 .as_str()
360 .len()
361 .checked_add(delimiter_len)
362 .and_then(|len| len.checked_add(query_len))
363 .ok_or(RequestTargetError::TooLong)?;
364 if len > MAX_REQUEST_TARGET_BYTES {
365 return Err(RequestTargetError::TooLong);
366 }
367 let target = output
368 .get_mut(..len)
369 .ok_or(RequestTargetError::OutputTooSmall)?;
370 let path_len = path.as_str().len();
371 let (path_output, suffix) = target.split_at_mut(path_len);
372 path_output.copy_from_slice(path.as_str().as_bytes());
373 if query.is_present() {
374 let (delimiter, query_output) = suffix.split_at_mut(1);
375 let delimiter = delimiter
376 .first_mut()
377 .ok_or(RequestTargetError::OutputTooSmall)?;
378 *delimiter = b'?';
379 if let Some(query) = query.as_borrowed_str() {
380 query_output.copy_from_slice(query.as_bytes());
381 }
382 }
383 let value = core::str::from_utf8(target).map_err(|_| RequestTargetError::OutputTooSmall)?;
384 let path_value = value
385 .get(..path_len)
386 .ok_or(RequestTargetError::OutputTooSmall)?;
387 let path = RequestPath(path_value);
388 let query = match query {
389 RequestQuery::Absent => RequestQuery::Absent,
390 RequestQuery::Canonical(_) => {
391 let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
392 let query_value = value
393 .get(query_start..)
394 .ok_or(RequestTargetError::OutputTooSmall)?;
395 RequestQuery::Canonical(CanonicalQuery(query_value))
396 }
397 RequestQuery::Form(_) => {
398 let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
399 let query_value = value
400 .get(query_start..)
401 .ok_or(RequestTargetError::OutputTooSmall)?;
402 RequestQuery::Form(FormQuery(query_value))
403 }
404 RequestQuery::ProviderLink(_) => {
405 return Err(RequestTargetError::ProviderLinkQueryCannotAssemble);
406 }
407 };
408 Ok(RequestTarget { value, path, query })
409 }
410
411 #[must_use]
413 pub const fn as_str(self) -> &'a str {
414 self.value
415 }
416
417 #[must_use]
419 pub const fn path(self) -> RequestPath<'a> {
420 self.path
421 }
422
423 #[must_use]
425 pub const fn query(self) -> RequestQuery<'a> {
426 self.query
427 }
428
429 #[must_use]
431 pub fn query_bytes(self) -> Option<&'a [u8]> {
432 self.query().as_borrowed_str().map(str::as_bytes)
433 }
434
435 #[must_use]
437 pub const fn len(self) -> usize {
438 self.value.len()
439 }
440
441 #[must_use]
443 pub const fn is_empty(self) -> bool {
444 false
445 }
446
447 pub(crate) fn from_provider_link(value: &'a str) -> Result<Self, RequestTargetError> {
448 if value.len() > MAX_REQUEST_TARGET_BYTES {
449 return Err(RequestTargetError::TooLong);
450 }
451 let (path, query) = match value.split_once('?') {
452 Some((path, query)) => {
453 validate_provider_link_query(query)
454 .map_err(|()| RequestTargetError::InvalidProviderLinkQuery)?;
455 (
456 RequestPath::new(path).map_err(RequestTargetError::Path)?,
457 RequestQuery::ProviderLink(ProviderLinkQuery(query)),
458 )
459 }
460 None => (
461 RequestPath::new(value).map_err(RequestTargetError::Path)?,
462 RequestQuery::Absent,
463 ),
464 };
465 Ok(Self { value, path, query })
466 }
467}
468
469impl fmt::Debug for RequestTarget<'_> {
470 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
471 formatter.write_str("RequestTarget([redacted])")
472 }
473}
474
475impl<'a> RequestQuery<'a> {
476 const fn as_borrowed_str(self) -> Option<&'a str> {
477 match self {
478 Self::Absent => None,
479 Self::Canonical(query) => Some(query.as_str()),
480 Self::Form(query) => Some(query.as_str()),
481 Self::ProviderLink(query) => Some(query.as_str()),
482 }
483 }
484}
485
486#[cfg(test)]
487mod tests;