use std::{num::NonZero, ops::RangeInclusive, sync::LazyLock};
use array_concat::{concat_arrays, concat_arrays_size};
use proptest::prelude::Strategy;
use crate::request_line::target::components::{
SUB_DELIMS, UNRESERVED, UrlChar, char_diff_intervals, safe_and_percent_encoded_char,
url_chars_to_string,
};
static PATH_UNSAFE_CHARS: LazyLock<Vec<RangeInclusive<char>>> =
LazyLock::new(|| char_diff_intervals(&PATH_SAFE_CHARS));
const PATH_SAFE_CHARS: [char; concat_arrays_size!(UNRESERVED, SUB_DELIMS) + 2] =
concat_arrays!(UNRESERVED, SUB_DELIMS, [':', '@']);
fn pchar() -> impl Strategy<Value = UrlChar> {
safe_and_percent_encoded_char(&PATH_SAFE_CHARS, &PATH_UNSAFE_CHARS)
}
fn segment(min_chars: usize, max_chars: usize) -> impl Strategy<Value = String> {
proptest::collection::vec(pchar(), min_chars..max_chars).prop_map(url_chars_to_string)
}
fn segment_nz(max_chars: usize) -> impl Strategy<Value = String> {
segment(1, max_chars)
}
#[derive(Debug)]
pub struct Path {
pub normalized: String,
}
pub fn path_rootless(max_segments: NonZero<usize>) -> impl Strategy<Value = (Path, String)> {
(segment_nz(50), proptest::collection::vec(segment(0, 50), 0..=max_segments.get())).prop_map(
|(segment_nz, segments)| {
let repr = if segments.is_empty() {
segment_nz.clone()
} else {
format!("{segment_nz}/{segments}", segments = segments.join("/"))
};
let mut normalized_path_segments = vec![];
if segment_nz != "." && segment_nz != ".." {
normalized_path_segments.push(segment_nz);
}
let segment_count = segments.len();
for (idx, segment) in segments.into_iter().enumerate() {
match segment.as_str() {
"." => {
if idx == segment_count - 1 {
normalized_path_segments.push(String::new());
}
}
".." => {
normalized_path_segments.pop();
}
_ => normalized_path_segments.push(segment),
}
}
(
Path {
normalized: if normalized_path_segments.is_empty() {
"/".to_string()
} else {
normalized_path_segments.join("/")
},
},
repr,
)
},
)
}
pub fn path_absolute(max_segments: NonZero<usize>) -> impl Strategy<Value = (Path, String)> {
path_rootless(max_segments).prop_map(|(path, repr)| {
(Path { normalized: format!("/{}", path.normalized) }, format!("/{repr}"))
})
}
#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;
use proptest::proptest;
use super::*;
proptest! {
#[test]
fn path_absolute_works((_, repr) in path_absolute(NonZeroUsize::new(25).unwrap())) {
assert!(repr.starts_with('/'));
}
}
}