use std::path::Path;
const EVIDENCE_DELIMITERS: &str = ",;)]}&#\"'";
const HOME_PREFIXES: [&str; 4] = ["/Users/", "/home/", "-Users-", "-home-"];
pub(crate) fn evidence_delimiter(character: char) -> bool {
character.is_ascii_whitespace() || EVIDENCE_DELIMITERS.contains(character)
}
fn home_path_delimiter(character: char) -> bool {
evidence_delimiter(character) || character == ':'
}
fn path_prefix_boundary(input: &str, end: usize, separator: char) -> bool {
input[end..].chars().next().is_none_or(|character| {
character == '/' || character == separator || home_path_delimiter(character)
})
}
fn exact_home_boundary(input: &str, end: usize, separator: char, dash_home: Option<&str>) -> bool {
let Some(character) = input[end..].chars().next() else {
return true;
};
if character == '/' || character == '~' || home_path_delimiter(character) {
return true;
}
if character != '-' {
return false;
}
separator == '-'
|| dash_home.is_some_and(|dash| input[end..].starts_with(dash))
|| input[end..].starts_with("-Users-")
|| input[end..].starts_with("-home-")
}
fn generic_home_prefix_end(input: &str, start: usize) -> Option<usize> {
let prefix = HOME_PREFIXES
.into_iter()
.find(|prefix| input[start..].starts_with(prefix))?;
let separator = prefix.chars().next().expect("prefixes are non-empty");
if start != 0
&& !input[..start].chars().next_back().is_some_and(|character| {
home_path_delimiter(character) || (separator == '-' && character == '/')
})
{
return None;
}
let component_start = start + prefix.len();
let component_end = input[component_start..]
.char_indices()
.find_map(|(offset, character)| {
(character == '/' || character == separator || home_path_delimiter(character))
.then_some(component_start + offset)
})
.unwrap_or(input.len());
(component_end > component_start && path_prefix_boundary(input, component_end, separator))
.then_some(component_end)
}
pub(crate) fn rewrite_home_paths(input: &str, home: Option<&Path>) -> String {
let home = home.and_then(Path::to_str);
let dash_home = home
.filter(|home| *home != "/")
.map(|home| home.replace('/', "-"));
let mut output = String::with_capacity(input.len());
let mut copied = 0;
let mut index = 0;
while index < input.len() {
let character = input[index..]
.chars()
.next()
.expect("index stays on a character boundary");
if character != '/' && character != '-' {
index += character.len_utf8();
continue;
}
let end = home
.filter(|home| input[index..].starts_with(home))
.map(|home| index + home.len())
.filter(|end| exact_home_boundary(input, *end, '/', dash_home.as_deref()))
.or_else(|| {
dash_home
.as_deref()
.filter(|dash| input[index..].starts_with(dash))
.map(|dash| index + dash.len())
.filter(|end| exact_home_boundary(input, *end, '-', dash_home.as_deref()))
})
.or_else(|| generic_home_prefix_end(input, index))
.filter(|end| *end > index);
if let Some(end) = end {
output.push_str(&input[copied..index]);
output.push('~');
copied = end;
index = end;
} else {
index += character.len_utf8();
}
}
if copied == 0 {
input.into()
} else {
output.push_str(&input[copied..]);
output
}
}