proptest_http_message/request_line/target/components/
path.rs1use 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#[derive(Debug)]
33pub struct Path {
34 pub normalized: String,
39}
40
41pub 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
88pub 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}