use std::io;
use std::path::{Path, PathBuf};
pub(crate) fn truncate_string_safe(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
return s.to_string();
}
let mut end = max_len;
while !s.is_char_boundary(end) {
end -= 1;
if end == 0 {
return format!("{}...", s.chars().next().unwrap_or('�'));
}
}
format!("{}...", &s[..end])
}
#[derive(Debug, thiserror::Error)]
pub enum FilePickerError {
#[error("Path traversal detected: {0}")]
PathTraversal(String),
#[error("Path is outside allowed directory")]
OutsideAllowedDirectory,
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Path contains invalid characters")]
InvalidCharacters,
#[error("Path contains Windows reserved device name: {0}")]
ReservedDeviceName(String),
#[error("Symbolic links are not allowed")]
SymlinkNotAllowed,
#[error("IO error: {0}")]
IoError(#[from] io::Error),
}
pub(crate) fn validate_path_characters(path: &Path) -> Result<(), FilePickerError> {
let path_str = path.to_string_lossy();
if path_str.contains('\0') {
return Err(FilePickerError::InvalidCharacters);
}
for component in path.components() {
if let std::path::Component::Normal(os_str) = component {
if os_str.as_encoded_bytes().contains(&b'\0') {
return Err(FilePickerError::InvalidCharacters);
}
}
}
Ok(())
}
#[cfg(windows)]
pub(crate) fn validate_windows_device_names(path: &Path) -> Result<(), FilePickerError> {
use std::path::Component;
let reserved = [
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
for component in path.components() {
if let Component::Normal(name) = component {
if let Some(name_str) = name.to_str() {
let name_upper = name_str.to_uppercase();
let base_name = name_upper.split('.').next().unwrap_or(&name_upper);
if reserved.contains(&base_name) {
return Err(FilePickerError::ReservedDeviceName(base_name.to_string()));
}
}
}
}
Ok(())
}
#[cfg(not(windows))]
pub(crate) fn validate_windows_device_names(_path: &Path) -> Result<(), FilePickerError> {
Ok(())
}
pub(crate) fn validate_path_no_traversal(path: &Path) -> Result<(), FilePickerError> {
for component in path.components() {
match component {
std::path::Component::ParentDir => {
return Err(FilePickerError::PathTraversal(
"path contains parent directory component".to_string(),
));
}
std::path::Component::Normal(_) => {
}
std::path::Component::Prefix(prefix) => {
let prefix_str = prefix.as_os_str().to_string_lossy();
if prefix_str.starts_with("\\\\?\\") || prefix_str.starts_with("\\\\.") {
if prefix_str.contains('\\') && prefix_str.len() < 10 {
return Err(FilePickerError::InvalidPath(
"suspicious device namespace path".to_string(),
));
}
}
}
std::path::Component::RootDir => {
}
std::path::Component::CurDir => {
}
}
}
Ok(())
}
pub(crate) fn validate_security_only(path: &Path) -> Result<PathBuf, FilePickerError> {
validate_path_characters(path)?;
validate_path_no_traversal(path)?;
validate_windows_device_names(path)?;
Ok(path.to_path_buf())
}
pub(crate) fn validate_and_canonicalize(
path: &Path,
base_dir: &Path,
) -> Result<PathBuf, FilePickerError> {
validate_path_characters(path)?;
validate_path_no_traversal(path)?;
validate_windows_device_names(path)?;
let base_canonical = if base_dir.exists() {
Some(
base_dir
.canonicalize()
.map_err(|_| FilePickerError::InvalidPath("invalid base directory".to_string()))?,
)
} else {
None
};
let canonical = path.canonicalize().map_err(|_| {
FilePickerError::InvalidPath("path does not exist or cannot be accessed".to_string())
})?;
if let Some(ref base) = base_canonical {
if !canonical.starts_with(base) {
return Err(FilePickerError::OutsideAllowedDirectory);
}
if canonical.as_os_str().len() < base.as_os_str().len() {
return Err(FilePickerError::OutsideAllowedDirectory);
}
}
Ok(canonical)
}