pub(crate) fn encode_path_segment(segment: &str) -> String {
let mut encoded = String::with_capacity(segment.len());
for byte in segment.as_bytes() {
if is_unreserved(*byte) {
encoded.push(*byte as char);
} else {
encoded.push('%');
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0f) as usize] as char);
}
}
encoded
}
const HEX: &[u8; 16] = b"0123456789ABCDEF";
fn is_unreserved(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_separator_can_never_survive_into_the_path() {
assert_eq!(encode_path_segment("BTC/USD"), "BTC%2FUSD");
assert_eq!(encode_path_segment("a?b"), "a%3Fb");
assert_eq!(encode_path_segment("a#b"), "a%23b");
assert_eq!(encode_path_segment("a b"), "a%20b");
}
#[test]
fn an_ordinary_symbol_is_left_alone() {
assert_eq!(encode_path_segment("SPY"), "SPY");
assert_eq!(encode_path_segment("BRK.B"), "BRK.B");
assert_eq!(encode_path_segment("a-b_c~d.9"), "a-b_c~d.9");
assert_eq!(encode_path_segment("5WX12345"), "5WX12345");
}
#[test]
fn the_symbols_that_motivated_this_round_trip() {
assert_eq!(encode_path_segment("BRK/B"), "BRK%2FB");
assert_eq!(encode_path_segment("/ESZ4"), "%2FESZ4");
assert_eq!(
encode_path_segment("./ESZ4 EW4U4 240927P5520"),
".%2FESZ4%20EW4U4%20240927P5520"
);
}
#[test]
fn a_percent_in_the_input_is_encoded_rather_than_trusted() {
assert_eq!(encode_path_segment("100%"), "100%25");
assert_eq!(
encode_path_segment(&encode_path_segment("BTC/USD")),
"BTC%252FUSD"
);
}
#[test]
fn non_ascii_is_encoded_byte_by_byte() {
assert_eq!(encode_path_segment("ñ"), "%C3%B1");
assert_eq!(encode_path_segment("€"), "%E2%82%AC");
assert_eq!(encode_path_segment("a€b"), "a%E2%82%ACb");
}
#[test]
fn hex_digits_are_uppercase() {
assert_eq!(encode_path_segment("\u{7f}"), "%7F");
assert_eq!(encode_path_segment("["), "%5B");
}
#[test]
fn an_empty_segment_encodes_to_nothing() {
assert_eq!(encode_path_segment(""), "");
}
#[test]
fn no_endpoint_rolls_its_own_encoding() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/api");
let entries = std::fs::read_dir(&dir).expect("src/api must be readable from the manifest");
let mut scanned = 0;
for entry in entries {
let path = entry.expect("directory entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string();
if name == "url.rs" {
continue;
}
let source = std::fs::read_to_string(&path).expect("module must be readable");
let source = match source.find("#[cfg(test)]") {
Some(at) => &source[..at],
None => &source[..],
};
scanned += 1;
assert!(
!source.contains(r#".replace("/""#),
"{name} encodes a path separator by hand; use encode_path_segment"
);
assert!(
!source.contains("%2F") && !source.contains("%2f"),
"{name} writes a percent-escape by hand; use encode_path_segment"
);
}
assert!(
scanned >= 5,
"expected to scan the api modules, only found {scanned}"
);
}
}