#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathPrefixStrategy {
DoubleSlash,
TripleSlash,
SlashDot,
SlashDotSlash,
}
impl PathPrefixStrategy {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::DoubleSlash => "path:double_slash",
Self::TripleSlash => "path:triple_slash",
Self::SlashDot => "path:slash_dot",
Self::SlashDotSlash => "path:slash_dot_slash",
}
}
#[must_use]
pub fn apply(self, path_and_query: &str) -> String {
if !path_and_query.starts_with('/') {
return path_and_query.to_string();
}
let prefix = match self {
Self::DoubleSlash => "//",
Self::TripleSlash => "///",
Self::SlashDot => "/./",
Self::SlashDotSlash => "/.//",
};
let rest = path_and_query.trim_start_matches('/');
format!("{prefix}{rest}")
}
pub const fn all() -> [Self; 4] {
[
Self::DoubleSlash,
Self::TripleSlash,
Self::SlashDot,
Self::SlashDotSlash,
]
}
}
#[must_use]
pub fn mutate_path_prefix(
path_and_query: &str,
strategy: PathPrefixStrategy,
) -> (String, &'static str) {
(strategy.apply(path_and_query), strategy.label())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn double_slash_maps_admin_to_double_admin() {
assert_eq!(PathPrefixStrategy::DoubleSlash.apply("/admin"), "//admin");
}
#[test]
fn triple_slash_normalises_to_triple() {
assert_eq!(PathPrefixStrategy::TripleSlash.apply("/admin"), "///admin");
}
#[test]
fn slash_dot_inserts_dot_segment() {
assert_eq!(PathPrefixStrategy::SlashDot.apply("/admin"), "/./admin");
}
#[test]
fn slash_dot_slash_combines_both_forms() {
assert_eq!(
PathPrefixStrategy::SlashDotSlash.apply("/admin"),
"/.//admin"
);
}
#[test]
fn already_double_slash_input_does_not_compound() {
assert_eq!(PathPrefixStrategy::DoubleSlash.apply("///admin"), "//admin");
}
#[test]
fn preserves_query_string() {
assert_eq!(
PathPrefixStrategy::DoubleSlash.apply("/admin?id=1&q=x"),
"//admin?id=1&q=x"
);
}
#[test]
fn non_root_relative_input_passes_through() {
assert_eq!(PathPrefixStrategy::DoubleSlash.apply("admin"), "admin");
assert_eq!(PathPrefixStrategy::DoubleSlash.apply(""), "");
}
#[test]
fn root_only_path_is_handled() {
assert_eq!(PathPrefixStrategy::DoubleSlash.apply("/"), "//");
assert_eq!(PathPrefixStrategy::SlashDot.apply("/"), "/./");
}
#[test]
fn all_strategies_label_distinctly() {
let labels: Vec<&str> = PathPrefixStrategy::all()
.iter()
.map(|s| s.label())
.collect();
let unique: std::collections::HashSet<_> = labels.iter().collect();
assert_eq!(
labels.len(),
unique.len(),
"every PathPrefixStrategy variant must have a distinct label"
);
}
#[test]
fn mutate_path_prefix_returns_label_matching_strategy() {
for s in PathPrefixStrategy::all() {
let (_, label) = mutate_path_prefix("/admin", s);
assert_eq!(label, s.label());
}
}
}