use crate::error::AppError;
use log::{debug, trace};
use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
pub fn percent_encode_path(path: &Path) -> String {
let path_str = path.to_string_lossy();
if path_str.is_empty() {
return String::new();
}
if path_str == "/" {
return "/".to_string();
}
path_str
.chars()
.map(|c| match c {
' ' => "%20".to_string(),
'"' => "%22".to_string(),
'#' => "%23".to_string(),
'%' => "%25".to_string(),
'<' => "%3C".to_string(),
'>' => "%3E".to_string(),
'?' => "%3F".to_string(),
c if !c.is_ascii() => {
let mut buf = [0; 4];
let encoded = c.encode_utf8(&mut buf);
encoded
.bytes()
.map(|b| format!("%{:02X}", b))
.collect::<String>()
}
c => c.to_string(),
})
.collect()
}
pub fn get_request_path(request_line: &str) -> &str {
if let Some(after_get) = request_line.strip_prefix("GET ") {
let after_get = after_get.trim_start();
if after_get.is_empty() {
return "/";
}
let path = if let Some(http_pos) = after_get.find(" HTTP/") {
after_get[..http_pos].trim()
} else {
after_get.trim()
};
if path.is_empty() {
return "/";
}
if let Some(relative_path) = path.strip_prefix('/') {
if relative_path.is_empty() {
return "/";
}
return relative_path;
}
return path;
}
"/" }
pub fn parse_query_params(url: &str) -> HashMap<String, String> {
trace!("Parsing query parameters from URL: {}", url);
let mut params = HashMap::new();
if let Some(query_start) = url.find('?') {
let query = &url[query_start + 1..];
for param in query.split('&') {
if let Some((key, value)) = param.split_once('=') {
let decoded_value = url_decode(value);
params.insert(key.to_string(), decoded_value);
}
}
}
params
}
fn url_decode(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
if let (Some(hex1), Some(hex2)) = (chars.next(), chars.next()) {
if let Ok(byte_val) = u8::from_str_radix(&format!("{hex1}{hex2}"), 16)
&& let Some(decoded_char) = char::from_u32(byte_val as u32)
{
result.push(decoded_char);
continue;
}
result.push('%');
result.push(hex1);
result.push(hex2);
} else {
result.push(ch);
}
} else if ch == '+' {
result.push(' ');
} else {
result.push(ch);
}
}
result
}
pub fn resolve_upload_directory(
base_dir: &Path,
upload_to: Option<&str>,
) -> Result<PathBuf, AppError> {
debug!(
"Resolving upload directory: base_dir={:?}, upload_to={:?}",
base_dir, upload_to
);
match upload_to {
Some(path_str) => {
let requested_path = PathBuf::from(path_str.strip_prefix('/').unwrap_or(path_str));
let safe_path = normalize_path(&requested_path)?;
let target_dir = base_dir.join(safe_path);
if !target_dir.starts_with(base_dir) {
return Err(AppError::Forbidden);
}
if !target_dir.exists() {
return Err(AppError::NotFound);
}
if !target_dir.is_dir() {
return Err(AppError::NotFound);
}
Ok(target_dir)
}
None => {
Ok(base_dir.to_path_buf())
}
}
}
fn normalize_path(path: &Path) -> Result<PathBuf, AppError> {
let mut components = Vec::new();
for component in path.components() {
match component {
Component::Normal(name) => {
components.push(name);
}
Component::ParentDir => {
if components.pop().is_none() {
return Err(AppError::Forbidden);
}
}
_ => {} }
}
Ok(components.iter().collect())
}
pub fn is_hidden_file(filename: &str) -> bool {
filename.starts_with("._") || filename.starts_with(".") || filename == ".DS_Store"
}