use super::error::AddressError;
use std::path::PathBuf;
const HEX: &[u8; 16] = b"0123456789ABCDEF";
pub(super) fn decode_path(text: &str) -> Result<PathBuf, AddressError> {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let byte = bytes[i];
if byte == b'%' {
let high = bytes.get(i + 1).copied().filter(|b| b.is_ascii_hexdigit());
let low = bytes.get(i + 2).copied().filter(|b| b.is_ascii_hexdigit());
match (high, low) {
(Some(high), Some(low)) => {
out.push((hex_value(high) << 4) | hex_value(low));
i += 3;
}
_ => return Err(AddressError::MalformedPercentEscape),
}
} else if byte == 0 {
return Err(AddressError::NulInPath);
} else if is_raw_path_byte(byte) || byte >= 0x80 {
out.push(byte);
i += 1;
} else {
return Err(AddressError::UnencodedPathByte);
}
}
if out.contains(&0) {
return Err(AddressError::NulInPath);
}
bytes_to_path(out)
}
pub(super) fn push_escaped(out: &mut String, bytes: &[u8]) {
use std::fmt::Write as _;
for &byte in bytes {
if is_raw_path_byte(byte) {
out.push(byte as char);
} else {
let _ = write!(
out,
"%{}{}",
HEX[(byte >> 4) as usize] as char,
HEX[(byte & 0x0F) as usize] as char
);
}
}
}
pub(super) fn hex_value(byte: u8) -> u8 {
match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
b'A'..=b'F' => byte - b'A' + 10,
_ => unreachable!("hex_value called on a non-hex byte"),
}
}
pub(super) fn is_raw_path_byte(byte: u8) -> bool {
matches!(
byte,
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'-'
| b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
| b'/'
)
}
pub(super) fn strip_leading_slash(path: PathBuf) -> PathBuf {
let bytes = path_bytes(&path);
debug_assert_eq!(bytes.first(), Some(&b'/'));
let stripped = bytes[1..].to_vec();
bytes_to_path(stripped).expect("rebuilding a decoded path cannot fail")
}
#[cfg(unix)]
pub fn bytes_to_path(bytes: Vec<u8>) -> Result<PathBuf, AddressError> {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
Ok(PathBuf::from(OsString::from_vec(bytes)))
}
#[cfg(unix)]
pub fn path_bytes(path: &std::path::Path) -> &[u8] {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes()
}
#[cfg(not(unix))]
pub fn bytes_to_path(bytes: Vec<u8>) -> Result<PathBuf, AddressError> {
let text = String::from_utf8(bytes).map_err(|_| AddressError::UnrepresentablePath)?;
Ok(PathBuf::from(text))
}
#[cfg(not(unix))]
pub fn path_bytes(path: &std::path::Path) -> &[u8] {
path.as_os_str().as_encoded_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn decoding_refuses_malformed_escapes_and_keeps_data_bytes() {
assert_eq!(decode_path("/a%20b%2Fc").unwrap(), Path::new("/a b/c"));
assert!(matches!(
decode_path("/%zz"),
Err(AddressError::MalformedPercentEscape)
));
assert!(matches!(decode_path("/%00"), Err(AddressError::NulInPath)));
}
#[test]
fn escaping_reproduces_only_necessary_escapes() {
let mut out = String::new();
push_escaped(&mut out, b"/var log/a#b%c?d");
assert_eq!(out, "/var%20log/a%23b%25c%3Fd");
}
}