use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CwdAt {
Spawn,
Rel(String),
Abs(String),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resolution {
Absolute,
CwdJoined,
CdTracked,
Unresolved,
}
impl Resolution {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Resolution::Absolute => "absolute",
Resolution::CwdJoined => "cwd-joined",
Resolution::CdTracked => "cd-tracked",
Resolution::Unresolved => "unresolved",
}
}
}
#[derive(Debug)]
pub(crate) struct CwdTracker {
cur: CwdAt,
prev: Option<CwdAt>,
depth: usize,
}
impl CwdTracker {
pub(crate) fn new() -> Self {
CwdTracker {
cur: CwdAt::Spawn,
prev: None,
depth: 0,
}
}
pub(crate) fn checkpoint(&self) -> CwdAt {
self.cur.clone()
}
pub(crate) fn observe_segment(&mut self, segment: &str, mask: &str) {
let depth_at_start = self.depth;
self.advance_depth(segment, mask);
if depth_at_start > 0 {
return; }
let toks = masked_tokens(segment, mask);
let all: Vec<&str> = toks
.iter()
.filter(|t| !is_fully_masked(t.masked))
.map(|t| t.orig)
.collect();
let cmd = strip_prefixes(&all);
let Some((&verb, operands)) = cmd.split_first() else {
return;
};
match verb {
"popd" => self.set(CwdAt::Unknown),
"cd" | "pushd" | "chdir" => self.apply_cd(operands),
_ => {}
}
}
fn apply_cd(&mut self, operands: &[&str]) {
let target = operands
.iter()
.map(|t| strip_quotes(t))
.find(|t| *t == "-" || !t.starts_with('-'));
let Some(t) = target else {
return self.set(CwdAt::Unknown); };
if t == "-" {
let back = match self.prev.clone() {
Some(CwdAt::Spawn) => CwdAt::Rel(String::new()),
Some(other) => other,
None => CwdAt::Unknown,
};
return self.set(back);
}
if t.is_empty()
|| t.contains('$')
|| t.contains('`')
|| t.starts_with('~')
|| has_syntax_noise(t)
{
return self.set(CwdAt::Unknown);
}
let next = if is_absolute_shell_path(t) {
CwdAt::Abs(t.to_string())
} else {
match &self.cur {
CwdAt::Spawn => CwdAt::Rel(t.to_string()),
CwdAt::Rel(r) => CwdAt::Rel(format!("{r}/{t}")),
CwdAt::Abs(a) => CwdAt::Abs(join_shell_path(a, &[t])),
CwdAt::Unknown => CwdAt::Unknown,
}
};
self.set(next);
}
fn set(&mut self, next: CwdAt) {
self.prev = Some(std::mem::replace(&mut self.cur, next));
}
fn advance_depth(&mut self, segment: &str, mask: &str) {
let seg = segment.as_bytes();
let m = mask.as_bytes();
for i in 0..m.len() {
if m[i] != seg[i] {
continue; }
match seg[i] {
b'(' => {
let head = if i > 0 { seg[i - 1] } else { b' ' };
if !matches!(head, b'$' | b'>' | b'<' | b'(') {
self.depth += 1;
}
}
b')' => self.depth = self.depth.saturating_sub(1),
_ => {}
}
}
}
}
#[must_use]
pub fn is_absolute_shell_path(p: &str) -> bool {
if p.starts_with('/') || p.starts_with("\\\\") {
return true;
}
let b = p.as_bytes();
b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && matches!(b[2], b'/' | b'\\')
}
#[must_use]
pub fn join_shell_path(base: &str, parts: &[&str]) -> String {
let windows = {
let b = base.as_bytes();
base.starts_with("\\\\") || (b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':')
};
let sep = if windows { '\\' } else { '/' };
let (root, rest): (String, &str) = if windows {
if let Some(tail) = base.strip_prefix("\\\\") {
let mut it = tail.splitn(3, ['\\', '/']);
let server = it.next().unwrap_or("");
let share = it.next().unwrap_or("");
(format!("\\\\{server}\\{share}"), it.next().unwrap_or(""))
} else {
(base[..2].to_string(), &base[3..])
}
} else {
("".to_string(), base.trim_start_matches('/'))
};
let mut comps: Vec<&str> = Vec::new();
for piece in std::iter::once(rest).chain(parts.iter().copied()) {
for c in piece.split(['/', '\\']) {
match c {
"" | "." => {}
".." => {
comps.pop();
}
other => comps.push(other),
}
}
}
let joined = comps.join(&sep.to_string());
if windows {
format!("{root}{sep}{joined}")
} else {
format!("/{joined}")
}
}
#[must_use]
pub fn is_class_marker(path: &str) -> bool {
const MARKER_HEADS: &[&str] = &["git:", "fmt:", "interp:", "pkg:", "extract:"];
MARKER_HEADS.iter().any(|h| path.starts_with(h))
}
impl BashMutation {
#[must_use]
pub fn resolve(&self, record_cwd: Option<&str>) -> (String, Resolution) {
let p = self.path.as_str();
if is_class_marker(p) {
return (p.to_string(), Resolution::Unresolved);
}
if is_absolute_shell_path(p) {
return (p.to_string(), Resolution::Absolute);
}
if p.starts_with('~') {
return (p.to_string(), Resolution::Unresolved);
}
let cwd = record_cwd.filter(|c| is_absolute_shell_path(c));
match (&self.cwd_at, cwd) {
(CwdAt::Abs(a), _) => (join_shell_path(a, &[p]), Resolution::CdTracked),
(CwdAt::Spawn, Some(c)) => (join_shell_path(c, &[p]), Resolution::CwdJoined),
(CwdAt::Rel(r), Some(c)) => (join_shell_path(c, &[r, p]), Resolution::CdTracked),
_ => (p.to_string(), Resolution::Unresolved),
}
}
}