use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Dropped {
Here {
start: usize,
end: usize,
path: String,
},
Elsewhere {
start: usize,
end: usize,
path: String,
},
}
impl Dropped {
pub(crate) fn range(&self) -> (usize, usize) {
match self {
Dropped::Here { start, end, .. } | Dropped::Elsewhere { start, end, .. } => {
(*start, *end)
}
}
}
}
pub(crate) fn resolve(raw: &str) -> Option<String> {
if raw.is_empty() {
return None;
}
if Path::new(raw).exists() {
return Some(raw.to_string());
}
let unquoted = unquote(raw);
if unquoted != raw && Path::new(unquoted).exists() {
return Some(unquoted.to_string());
}
if unquoted.contains('\\') {
let unescaped = unescape(unquoted);
if Path::new(&unescaped).exists() {
return Some(unescaped);
}
}
None
}
fn unquote(raw: &str) -> &str {
raw.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| raw.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
.unwrap_or(raw)
}
fn unescape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
}
} else {
out.push(c);
}
}
out
}
fn is_path_start(text: &str, at: usize) -> bool {
if at > 0 && !text.as_bytes()[at - 1].is_ascii_whitespace() {
return false;
}
let rest = &text[at..];
rest.starts_with('/')
|| rest.starts_with("~/")
|| rest.starts_with("./")
|| rest.starts_with("../")
|| ((rest.starts_with('"') || rest.starts_with('\'')) && rest.len() > 2)
}
fn extension_ends(lower: &str, exts: &[&str]) -> Vec<usize> {
let mut ends = Vec::new();
for ext in exts {
let mut from = 0usize;
while let Some(rel) = lower[from..].find(ext) {
let end = from + rel + ext.len();
let boundary = lower[end..]
.chars()
.next()
.is_none_or(|c| c.is_whitespace() || c == '"' || c == '\'');
if boundary {
ends.push(end);
}
from = from + rel + 1;
if from >= lower.len() {
break;
}
}
}
ends.sort_unstable();
ends.dedup();
ends
}
pub(crate) fn find(text: &str, exts: &[&str]) -> Option<Dropped> {
let lower = text.to_lowercase();
for end in extension_ends(&lower, exts) {
let mut starts: Vec<usize> = (0..end).filter(|i| is_path_start(text, *i)).collect();
starts.sort_unstable();
for &start in &starts {
if let Some(path) = resolve(text[start..end].trim()) {
return Some(Dropped::Here { start, end, path });
}
}
if let Some(&start) = starts.first() {
let raw = text[start..end].trim();
let cleaned = unescape(unquote(raw));
if cleaned.starts_with('/') || cleaned.starts_with('~') {
return Some(Dropped::Elsewhere {
start,
end,
path: cleaned,
});
}
}
}
None
}