proptest_http_message/request_line/target/components/
path.rs

1//! URL path strategies.
2
3use std::{num::NonZero, ops::RangeInclusive, sync::LazyLock};
4
5use array_concat::{concat_arrays, concat_arrays_size};
6use proptest::prelude::Strategy;
7
8use crate::request_line::target::components::{
9  SUB_DELIMS, UNRESERVED, UrlChar, char_diff_intervals, safe_and_percent_encoded_char,
10  url_chars_to_string,
11};
12
13static PATH_UNSAFE_CHARS: LazyLock<Vec<RangeInclusive<char>>> =
14  LazyLock::new(|| char_diff_intervals(&PATH_SAFE_CHARS));
15
16const PATH_SAFE_CHARS: [char; concat_arrays_size!(UNRESERVED, SUB_DELIMS) + 2] =
17  concat_arrays!(UNRESERVED, SUB_DELIMS, [':', '@']);
18
19fn pchar() -> impl Strategy<Value = UrlChar> {
20  safe_and_percent_encoded_char(&PATH_SAFE_CHARS, &PATH_UNSAFE_CHARS)
21}
22
23fn segment(min_chars: usize, max_chars: usize) -> impl Strategy<Value = String> {
24  proptest::collection::vec(pchar(), min_chars..max_chars).prop_map(url_chars_to_string)
25}
26
27fn segment_nz(max_chars: usize) -> impl Strategy<Value = String> {
28  segment(1, max_chars)
29}
30
31/// URL path.
32#[derive(Debug)]
33pub struct Path {
34  /// normalized path.
35  /// # Examples
36  /// * the path `"/foo/./bar"` will be normalized to `"/foo/bar"`
37  /// * the path `"/foo/../bar"` will be normalized to `"/bar"`
38  pub normalized: String,
39}
40
41/// rootless path with no query params and no fragment.
42/// # Returns
43/// [`Path`] and its raw representation.
44pub fn path_rootless(max_segments: NonZero<usize>) -> impl Strategy<Value = (Path, String)> {
45  (segment_nz(50), proptest::collection::vec(segment(0, 50), 0..=max_segments.get())).prop_map(
46    |(segment_nz, segments)| {
47      let repr = if segments.is_empty() {
48        segment_nz.clone()
49      } else {
50        format!("{segment_nz}/{segments}", segments = segments.join("/"))
51      };
52
53      let mut normalized_path_segments = vec![];
54
55      if segment_nz != "." && segment_nz != ".." {
56        normalized_path_segments.push(segment_nz);
57      }
58
59      let segment_count = segments.len();
60      for (idx, segment) in segments.into_iter().enumerate() {
61        match segment.as_str() {
62          "." => {
63            if idx == segment_count - 1 {
64              normalized_path_segments.push(String::new());
65            }
66          }
67          ".." => {
68            normalized_path_segments.pop();
69          }
70          _ => normalized_path_segments.push(segment),
71        }
72      }
73
74      (
75        Path {
76          normalized: if normalized_path_segments.is_empty() {
77            "/".to_string()
78          } else {
79            normalized_path_segments.join("/")
80          },
81        },
82        repr,
83      )
84    },
85  )
86}
87
88/// absolute path with no query params and no fragment.
89/// #Returns
90/// [`Path`] and its raw representation.
91pub fn path_absolute(max_segments: NonZero<usize>) -> impl Strategy<Value = (Path, String)> {
92  path_rootless(max_segments).prop_map(|(path, repr)| {
93    (Path { normalized: format!("/{}", path.normalized) }, format!("/{repr}"))
94  })
95}
96
97#[cfg(test)]
98mod tests {
99  use std::num::NonZeroUsize;
100
101  use proptest::proptest;
102
103  use super::*;
104
105  proptest! {
106    #[test]
107    fn path_absolute_works((_, repr) in path_absolute(NonZeroUsize::new(25).unwrap())) {
108      assert!(repr.starts_with('/'));
109    }
110  }
111}