use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct PathInfo {
pub existing_canonical: PathBuf,
pub non_existing_suffix: PathBuf,
pub full_path: PathBuf,
}
impl PathInfo {
pub fn fully_exists(&self) -> bool {
self.non_existing_suffix.as_os_str().is_empty()
}
pub fn full_path_string(&self) -> String {
self.full_path.to_string_lossy().to_string()
}
pub fn empty() -> Self {
Self {
existing_canonical: PathBuf::new(),
non_existing_suffix: PathBuf::new(),
full_path: PathBuf::new(),
}
}
}
impl Default for PathInfo {
fn default() -> Self {
Self::empty()
}
}
pub fn parse_path(input: &str) -> PathInfo {
if input.is_empty() {
return PathInfo::empty();
}
let expanded = expand_tilde(input);
let path = Path::new(&expanded);
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|err| {
log::warn!("Could not determine current directory: {}. Using relative path.", err);
path.to_path_buf()
})
};
let (existing, suffix) = find_existing_prefix(&absolute);
let canonical = existing
.canonicalize()
.unwrap_or_else(|err| {
log::warn!("Could not canonicalize path {:?}: {}", existing, err);
existing.clone()
});
let full = if suffix.as_os_str().is_empty() {
canonical.clone()
} else {
canonical.join(&suffix)
};
PathInfo {
existing_canonical: canonical,
non_existing_suffix: suffix,
full_path: full,
}
}
#[cfg(feature = "file-picker")]
pub fn expand_tilde(path: &str) -> String {
if path.starts_with("~/") || path == "~" {
if let Some(home) = dirs::home_dir() {
if path == "~" {
return home.to_string_lossy().to_string();
} else {
return home.join(&path[2..]).to_string_lossy().to_string();
}
}
}
path.to_string()
}
#[cfg(not(feature = "file-picker"))]
pub fn expand_tilde(path: &str) -> String {
if path.starts_with("~/") || path == "~" {
if let Ok(home) = std::env::var("HOME") {
if path == "~" {
return home;
} else {
return format!("{}/{}", home, &path[2..]);
}
}
}
path.to_string()
}
fn build_suffix(components: &[std::ffi::OsString]) -> PathBuf {
components
.iter()
.rev()
.fold(PathBuf::new(), |acc, comp| acc.join(comp))
}
fn find_existing_prefix(path: &Path) -> (PathBuf, PathBuf) {
let mut current = path.to_path_buf();
let mut suffix_components = Vec::new();
loop {
if current.exists() {
return (current, build_suffix(&suffix_components));
}
match current.file_name() {
Some(component) => {
suffix_components.push(component.to_os_string());
current = current
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("/"));
}
None => {
return (PathBuf::from("/"), build_suffix(&suffix_components));
}
}
if current.as_os_str().is_empty() || current == Path::new("/") {
return (current, build_suffix(&suffix_components));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_tilde() {
let expanded = expand_tilde("~");
assert!(!expanded.is_empty());
assert!(!expanded.starts_with('~') || expanded == "~");
let with_path = expand_tilde("~/Documents");
assert!(with_path.ends_with("/Documents") || with_path == "~/Documents");
}
#[test]
fn test_parse_existing_path() {
let temp = std::env::temp_dir();
let info = parse_path(temp.to_str().unwrap());
assert!(info.fully_exists());
assert_eq!(info.non_existing_suffix, PathBuf::new());
}
#[test]
fn test_parse_non_existing_file() {
let temp = std::env::temp_dir();
let non_existing = temp.join("this_file_should_not_exist_12345.txt");
let info = parse_path(non_existing.to_str().unwrap());
assert!(!info.fully_exists());
assert_eq!(info.non_existing_suffix, PathBuf::from("this_file_should_not_exist_12345.txt"));
}
#[test]
fn test_empty_path() {
let info = parse_path("");
assert!(info.full_path.as_os_str().is_empty());
}
}