pub fn percent_decode_segment(segment: &str) -> String {
if !segment.contains('%') {
return segment.to_string();
}
percent_encoding::percent_decode_str(segment)
.decode_utf8_lossy()
.into_owned()
}
pub fn split_path_segments(raw_path: &str) -> Vec<String> {
raw_path
.split('/')
.filter(|s| !s.is_empty())
.map(percent_decode_segment)
.collect()
}
pub fn split_raw_path_segments(raw_path: &str) -> Vec<String> {
raw_path
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_encoded_colon_in_arn_label() {
assert_eq!(
split_path_segments(
"/tags/arn%3Aaws%3Abatch%3Aus-east-1%3A123456789012%3Ajob-queue%2Fq"
),
vec![
"tags".to_string(),
"arn:aws:batch:us-east-1:123456789012:job-queue/q".to_string()
]
);
}
#[test]
fn encoded_slash_stays_inside_its_non_greedy_label() {
assert_eq!(
split_path_segments("/v1/pipes/a%2Fb/start"),
vec!["v1", "pipes", "a/b", "start"]
);
}
#[test]
fn double_encoded_percent_decodes_exactly_once() {
assert_eq!(split_path_segments("/x/100%2525"), vec!["x", "100%25"]);
assert_eq!(percent_decode_segment("%2525"), "%25");
}
#[test]
fn greedy_label_is_the_rejoined_decoded_segments() {
let segs = split_path_segments("/bucket/dir%20one/sub/file%3Dx.txt");
assert_eq!(segs[1..].join("/"), "dir one/sub/file=x.txt");
}
#[test]
fn plus_is_literal_in_paths() {
assert_eq!(split_path_segments("/k/a+b%2Bc"), vec!["k", "a+b+c"]);
}
#[test]
fn empty_segments_are_dropped() {
assert_eq!(split_path_segments("//a//b/"), vec!["a", "b"]);
assert!(split_path_segments("/").is_empty());
}
#[test]
fn malformed_escapes_and_multibyte_are_safe() {
assert_eq!(percent_decode_segment("%zz%"), "%zz%");
assert_eq!(percent_decode_segment("%€"), "%€");
assert_eq!(percent_decode_segment("caf%C3%A9"), "café");
assert_eq!(percent_decode_segment("%FF"), "\u{FFFD}");
}
#[test]
fn raw_segments_are_not_decoded() {
assert_eq!(
split_raw_path_segments("/prod/a%2Fb//c%3A"),
vec!["prod", "a%2Fb", "c%3A"]
);
}
}