1pub fn percent_decode_segment(segment: &str) -> String {
21 if !segment.contains('%') {
22 return segment.to_string();
23 }
24 percent_encoding::percent_decode_str(segment)
25 .decode_utf8_lossy()
26 .into_owned()
27}
28
29pub fn split_path_segments(raw_path: &str) -> Vec<String> {
33 raw_path
34 .split('/')
35 .filter(|s| !s.is_empty())
36 .map(percent_decode_segment)
37 .collect()
38}
39
40pub fn split_raw_path_segments(raw_path: &str) -> Vec<String> {
45 raw_path
46 .split('/')
47 .filter(|s| !s.is_empty())
48 .map(str::to_string)
49 .collect()
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn decodes_encoded_colon_in_arn_label() {
58 assert_eq!(
59 split_path_segments(
60 "/tags/arn%3Aaws%3Abatch%3Aus-east-1%3A123456789012%3Ajob-queue%2Fq"
61 ),
62 vec![
63 "tags".to_string(),
64 "arn:aws:batch:us-east-1:123456789012:job-queue/q".to_string()
65 ]
66 );
67 }
68
69 #[test]
70 fn encoded_slash_stays_inside_its_non_greedy_label() {
71 assert_eq!(
73 split_path_segments("/v1/pipes/a%2Fb/start"),
74 vec!["v1", "pipes", "a/b", "start"]
75 );
76 }
77
78 #[test]
79 fn double_encoded_percent_decodes_exactly_once() {
80 assert_eq!(split_path_segments("/x/100%2525"), vec!["x", "100%25"]);
81 assert_eq!(percent_decode_segment("%2525"), "%25");
82 }
83
84 #[test]
85 fn greedy_label_is_the_rejoined_decoded_segments() {
86 let segs = split_path_segments("/bucket/dir%20one/sub/file%3Dx.txt");
87 assert_eq!(segs[1..].join("/"), "dir one/sub/file=x.txt");
88 }
89
90 #[test]
91 fn plus_is_literal_in_paths() {
92 assert_eq!(split_path_segments("/k/a+b%2Bc"), vec!["k", "a+b+c"]);
93 }
94
95 #[test]
96 fn empty_segments_are_dropped() {
97 assert_eq!(split_path_segments("//a//b/"), vec!["a", "b"]);
98 assert!(split_path_segments("/").is_empty());
99 }
100
101 #[test]
102 fn malformed_escapes_and_multibyte_are_safe() {
103 assert_eq!(percent_decode_segment("%zz%"), "%zz%");
104 assert_eq!(percent_decode_segment("%€"), "%€");
105 assert_eq!(percent_decode_segment("caf%C3%A9"), "café");
106 assert_eq!(percent_decode_segment("%FF"), "\u{FFFD}");
107 }
108
109 #[test]
110 fn raw_segments_are_not_decoded() {
111 assert_eq!(
112 split_raw_path_segments("/prod/a%2Fb//c%3A"),
113 vec!["prod", "a%2Fb", "c%3A"]
114 );
115 }
116}