use std::borrow::Cow;
use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
pub(crate) const HADOOP_URI_PATH_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
pub(crate) fn uri_encode_path(path: &str) -> Cow<'_, str> {
utf8_percent_encode(path, HADOOP_URI_PATH_ENCODE_SET).into()
}
pub(crate) fn uri_decode_path(path: &str) -> Option<Cow<'_, str>> {
percent_decode_str(path).decode_utf8().ok()
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
fn test_uri_encode_path_every_ascii_byte() {
let must_encode: &[u8] = b" \"#%<>?[\\]^`{|}";
for byte in 0x00..=0x7Fu8 {
let input = String::from(byte as char);
let result = uri_encode_path(&input);
let is_control = byte <= 0x1F || byte == 0x7F;
if must_encode.contains(&byte) || is_control {
assert_eq!(result, format!("%{:02X}", byte), "byte 0x{byte:02X}");
} else {
assert_eq!(result, input, "byte 0x{byte:02X}");
}
}
}
#[rstest]
#[case::backtick("a`b", "a%60b")]
#[case::right_brace("a}b", "a%7Db")]
#[case::double_quote("a\"b", "a%22b")]
#[case::space("hello world", "hello%20world")]
#[case::less_than("a<b", "a%3Cb")]
#[case::greater_than("a>b", "a%3Eb")]
#[case::pipe("a|b", "a%7Cb")]
fn test_uri_encode_path_encodes_uri_only_set(#[case] input: &str, #[case] expected: &str) {
assert_eq!(uri_encode_path(input), expected);
}
#[rstest]
#[case::empty("")]
#[case::alnum("hello")]
#[case::partition_path("p=abc/q=def/")]
#[case::hive_default_partition("__HIVE_DEFAULT_PARTITION__")]
#[case::alnum_unreserved("A-Za-z0-9_.~")]
#[case::date("2024-01-15")]
fn test_uri_encode_path_unreserved_borrows_and_passes_through(#[case] input: &str) {
let result = uri_encode_path(input);
assert_eq!(result, input);
assert!(matches!(result, Cow::Borrowed(_)));
}
}