use std::{
env,
path::{Component, Path, PathBuf},
};
use crate::{
error::{PathError, PathResult},
system::get_username,
};
pub fn is_safe_component(name: &str) -> bool {
let mut components = Path::new(name).components();
matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()
}
pub fn resolve_path(path: &str) -> PathResult<PathBuf> {
let path = path.trim();
if path.is_empty() {
return Err(PathError::Empty);
}
let resolved = expand_variables(path)?;
let path_buf = PathBuf::from(resolved);
if path_buf.is_absolute() {
Ok(path_buf)
} else {
env::current_dir()
.map(|cwd| cwd.join(path_buf))
.map_err(|err| {
PathError::FailedToGetCurrentDir {
source: err,
}
})
}
}
pub fn home_dir() -> PathBuf {
env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(format!("/home/{}", get_username())))
}
pub fn xdg_config_home() -> PathBuf {
env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().join(".config"))
}
pub fn xdg_data_home() -> PathBuf {
env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().join(".local/share"))
}
pub fn xdg_cache_home() -> PathBuf {
env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home_dir().join(".cache"))
}
pub fn desktop_dir(system: bool) -> PathBuf {
if system {
PathBuf::from("/usr/local/share/applications")
} else {
xdg_data_home().join("applications")
}
}
pub fn icons_dir(system: bool) -> PathBuf {
if system {
PathBuf::from("/usr/local/share/icons/hicolor")
} else {
xdg_data_home().join("icons/hicolor")
}
}
fn expand_variables(path: &str) -> PathResult<String> {
let mut result = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(c) = chars.next() {
match c {
'$' => {
if chars.peek() == Some(&'{') {
chars.next();
let var_name = consume_until(&mut chars, '}')?;
expand_env_var(&var_name, &mut result, path)?;
} else {
let var_name = consume_var_name(&mut chars);
if var_name.is_empty() {
result.push('$');
} else {
expand_env_var(&var_name, &mut result, path)?;
}
}
}
'~' if result.is_empty() => result.push_str(&home_dir().to_string_lossy()),
_ => result.push(c),
}
}
Ok(result)
}
fn consume_until(
chars: &mut std::iter::Peekable<std::str::Chars>,
delimiter: char,
) -> PathResult<String> {
let mut var_name = String::new();
for c in chars.by_ref() {
if c == delimiter {
return Ok(var_name);
}
var_name.push(c);
}
Err(PathError::UnclosedVariable {
input: format!("${{{var_name}"),
})
}
fn consume_var_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
let mut var_name = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' {
var_name.push(chars.next().unwrap());
} else {
break;
}
}
var_name
}
fn expand_env_var(var_name: &str, result: &mut String, original: &str) -> PathResult<()> {
match var_name {
"HOME" => result.push_str(&home_dir().to_string_lossy()),
"XDG_CONFIG_HOME" => result.push_str(&xdg_config_home().to_string_lossy()),
"XDG_DATA_HOME" => result.push_str(&xdg_data_home().to_string_lossy()),
"XDG_CACHE_HOME" => result.push_str(&xdg_cache_home().to_string_lossy()),
_ => {
let value = env::var(var_name).map_err(|_| {
PathError::MissingEnvVar {
input: original.into(),
var: var_name.into(),
}
})?;
result.push_str(&value);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_is_safe_component() {
assert!(super::is_safe_component("clipcat"));
assert!(super::is_safe_component("clip-cat.1"));
assert!(super::is_safe_component("soarpkgs"));
assert!(!super::is_safe_component(""));
assert!(!super::is_safe_component("."));
assert!(!super::is_safe_component(".."));
assert!(!super::is_safe_component("a/b"));
assert!(!super::is_safe_component("../etc"));
assert!(!super::is_safe_component("../../home/user/.bashrc"));
assert!(!super::is_safe_component("/etc/passwd"));
}
use std::env;
use serial_test::serial;
use super::*;
#[test]
fn test_expand_variables_simple() {
env::set_var("TEST_VAR", "test_value");
let result = expand_variables("$TEST_VAR/path").unwrap();
assert_eq!(result, "test_value/path");
env::remove_var("TEST_VAR");
}
#[test]
fn test_expand_variables_braces() {
env::set_var("TEST_VAR_BRACES", "test_value");
let result = expand_variables("${TEST_VAR_BRACES}/path").unwrap();
assert_eq!(result, "test_value/path");
env::remove_var("TEST_VAR_BRACES");
}
#[test]
fn test_expand_variables_missing_braces() {
env::set_var("TEST_VAR_MISSING_BRACES", "test_value");
let result = expand_variables("${TEST_VAR_MISSING_BRACES");
assert!(result.is_err());
env::remove_var("TEST_VAR_MISSING_BRACES");
}
#[test]
fn test_expand_variables_missing_var() {
let result = expand_variables("$THIS_VAR_DOESNT_EXIST");
assert!(result.is_err());
}
#[test]
fn test_consume_var_name() {
let mut chars = "VAR_NAME_123/extra".chars().peekable();
let var_name = consume_var_name(&mut chars);
assert_eq!(var_name, "VAR_NAME_123");
}
#[test]
#[serial]
fn test_xdg_directories() {
env::set_var("HOME", "/tmp/home");
let home = home_dir();
assert_eq!(home, PathBuf::from("/tmp/home"));
env::remove_var("XDG_CONFIG_HOME");
env::remove_var("XDG_DATA_HOME");
env::remove_var("XDG_CACHE_HOME");
let config = xdg_config_home();
let data = xdg_data_home();
let cache = xdg_cache_home();
assert_eq!(config, home.join(".config"));
assert_eq!(data, home.join(".local/share"));
assert_eq!(cache, home.join(".cache"));
assert!(config.is_absolute());
assert!(data.is_absolute());
assert!(cache.is_absolute());
env::set_var("XDG_CONFIG_HOME", "/tmp/config");
env::set_var("XDG_DATA_HOME", "/tmp/data");
env::set_var("XDG_CACHE_HOME", "/tmp/cache");
assert_eq!(xdg_config_home(), PathBuf::from("/tmp/config"));
assert_eq!(xdg_data_home(), PathBuf::from("/tmp/data"));
assert_eq!(xdg_cache_home(), PathBuf::from("/tmp/cache"));
env::remove_var("XDG_CONFIG_HOME");
env::remove_var("XDG_DATA_HOME");
env::remove_var("XDG_CACHE_HOME");
env::remove_var("HOME");
}
#[test]
#[serial]
fn test_resolve_path() {
env::set_var("HOME", "/tmp/home");
assert!(resolve_path("").is_err());
assert_eq!(
resolve_path("/absolute/path").unwrap(),
PathBuf::from("/absolute/path")
);
let expected_relative = env::current_dir().unwrap().join("relative/path");
assert_eq!(resolve_path("relative/path").unwrap(), expected_relative);
let home = home_dir();
assert_eq!(resolve_path("~/path").unwrap(), home.join("path"));
assert_eq!(resolve_path("~").unwrap(), home);
let expected_tilde_middle = env::current_dir().unwrap().join("not/at/~/start");
assert_eq!(
resolve_path("not/at/~/start").unwrap(),
expected_tilde_middle
);
env::remove_var("HOME");
let result = resolve_path("${VAR");
assert!(result.is_err());
let result = resolve_path("${VAR}");
assert!(result.is_err());
}
#[test]
#[serial]
fn test_home_dir() {
env::set_var("HOME", "/tmp/home");
assert_eq!(home_dir(), PathBuf::from("/tmp/home"));
env::remove_var("HOME");
let expected = PathBuf::from(format!("/home/{}", get_username()));
assert_eq!(home_dir(), expected);
}
#[test]
#[serial]
fn test_expand_variables_edge_cases() {
env::set_var("HOME", "/tmp/home");
assert_eq!(expand_variables("path/$").unwrap(), "path/$");
assert_eq!(
expand_variables("path/$!invalid").unwrap(),
"path/$!invalid"
);
env::set_var("VAR1", "val1");
env::set_var("VAR2", "val2");
assert_eq!(expand_variables("$VAR1/${VAR2}").unwrap(), "val1/val2");
env::remove_var("VAR1");
env::remove_var("VAR2");
let home_str = home_dir().to_string_lossy().to_string();
assert_eq!(
expand_variables("~/path").unwrap(),
format!("{}/path", home_str)
);
assert_eq!(expand_variables("~").unwrap(), home_str);
assert_eq!(expand_variables("a/~/b").unwrap(), "a/~/b");
env::remove_var("HOME");
}
#[test]
#[serial]
fn test_resolve_path_invalid_cwd() {
let temp_dir = tempfile::tempdir().unwrap();
let invalid_path = temp_dir.path().join("invalid");
std::fs::create_dir(&invalid_path).unwrap();
let original_cwd = env::current_dir().unwrap();
env::set_current_dir(&invalid_path).unwrap();
std::fs::remove_dir(&invalid_path).unwrap();
let result = resolve_path("relative/path");
assert!(result.is_err());
env::set_current_dir(original_cwd).unwrap();
}
#[test]
#[serial]
fn test_expand_env_var_special_vars() {
env::set_var("HOME", "/tmp/home");
env::remove_var("XDG_CONFIG_HOME");
env::remove_var("XDG_DATA_HOME");
env::remove_var("XDG_CACHE_HOME");
let mut result = String::new();
expand_env_var("HOME", &mut result, "$HOME").unwrap();
assert_eq!(result, "/tmp/home");
result.clear();
expand_env_var("XDG_CONFIG_HOME", &mut result, "$XDG_CONFIG_HOME").unwrap();
assert_eq!(result, "/tmp/home/.config");
result.clear();
expand_env_var("XDG_DATA_HOME", &mut result, "$XDG_DATA_HOME").unwrap();
assert_eq!(result, "/tmp/home/.local/share");
result.clear();
expand_env_var("XDG_CACHE_HOME", &mut result, "$XDG_CACHE_HOME").unwrap();
assert_eq!(result, "/tmp/home/.cache");
env::remove_var("HOME");
}
#[test]
#[serial]
fn test_desktop_dir() {
env::set_var("XDG_DATA_HOME", "/tmp/data");
let desktop = desktop_dir(false);
assert_eq!(desktop, PathBuf::from("/tmp/data/applications"));
let desktop = desktop_dir(true);
assert_eq!(desktop, PathBuf::from("/usr/local/share/applications"));
}
#[test]
#[serial]
fn test_icons_dir() {
env::set_var("XDG_DATA_HOME", "/tmp/data");
let icons = icons_dir(false);
assert_eq!(icons, PathBuf::from("/tmp/data/icons/hicolor"));
let icons = icons_dir(true);
assert_eq!(icons, PathBuf::from("/usr/local/share/icons/hicolor"));
}
}