use std::fmt;
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use crate::error::{Error, Result};
const SEGMENT: &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')')
.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 segment(value: impl fmt::Display) -> Result<String> {
let raw = value.to_string();
if raw.is_empty() {
return Err(Error::InvalidRequest(
"a path segment cannot be empty".to_owned(),
));
}
if raw == "." || raw == ".." {
return Err(Error::InvalidRequest(format!(
"`{raw}` cannot be used as a path segment: a URL parser removes it \
rather than sending it, so the request would address a different route"
)));
}
Ok(utf8_percent_encode(&raw, SEGMENT).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_crypto_pair_is_one_segment_not_two() {
assert_eq!(segment("BTC/USD").unwrap(), "BTC%2FUSD");
}
#[test]
fn a_dotted_ticker_is_left_alone() {
assert_eq!(segment("BRK.A").unwrap(), "BRK.A");
}
#[test]
fn a_plain_symbol_is_unchanged() {
assert_eq!(segment("AAPL").unwrap(), "AAPL");
assert_eq!(
segment("b0b6dd9d-8b9b-48a9-ba46-b9d54906e415").unwrap(),
"b0b6dd9d-8b9b-48a9-ba46-b9d54906e415"
);
}
#[test]
fn query_and_fragment_delimiters_cannot_escape_the_segment() {
assert_eq!(segment("AAPL?foo=bar").unwrap(), "AAPL%3Ffoo%3Dbar");
assert_eq!(segment("AAPL#frag").unwrap(), "AAPL%23frag");
}
#[test]
fn a_percent_is_encoded_so_a_caller_cannot_spell_a_dot() {
assert_eq!(segment("%2E%2E").unwrap(), "%252E%252E");
}
#[test]
fn traversal_is_neutralized_by_encoding_the_slash() {
assert_eq!(segment("../positions").unwrap(), "..%2Fpositions");
}
#[test]
fn the_dot_segments_are_refused_rather_than_encoded() {
for value in [".", ".."] {
let error = segment(value).unwrap_err();
assert!(
matches!(error, Error::InvalidRequest(_)),
"expected InvalidRequest for {value:?}, got {error:?}"
);
}
}
#[test]
fn an_empty_segment_is_refused() {
assert!(matches!(segment("").unwrap_err(), Error::InvalidRequest(_)));
}
#[test]
fn a_space_and_non_ascii_survive_as_an_encoded_segment() {
assert_eq!(segment("a b").unwrap(), "a%20b");
assert_eq!(segment("é").unwrap(), "%C3%A9");
}
}