use std::borrow::Cow;
use std::cell::RefCell;
#[derive(Clone, Default)]
pub struct PathCtx {
pub cwd: Option<String>,
pub root: Option<String>,
}
thread_local! {
static CURRENT: RefCell<PathCtx> = RefCell::new(PathCtx::default());
}
#[must_use]
pub fn enter(ctx: PathCtx) -> Guard {
Guard(CURRENT.with(|c| c.replace(ctx)))
}
pub struct Guard(PathCtx);
impl Drop for Guard {
fn drop(&mut self) {
CURRENT.with(|c| *c.borrow_mut() = std::mem::take(&mut self.0));
}
}
#[must_use]
pub fn enter_cwd(cwd: Option<String>) -> Guard {
Guard(CURRENT.with(|c| {
let mut b = c.borrow_mut();
PathCtx { cwd: std::mem::replace(&mut b.cwd, cwd), root: b.root.clone() }
}))
}
pub fn cwd() -> Option<String> {
CURRENT.with(|c| c.borrow().cwd.clone())
}
pub fn root() -> Option<String> {
CURRENT.with(|c| {
let b = c.borrow();
b.root.clone().or_else(|| b.cwd.clone())
})
}
struct LoopVar {
name: String,
read_repr: String,
write_repr: String,
}
thread_local! {
static LOOP_VARS: RefCell<Vec<LoopVar>> = const { RefCell::new(Vec::new()) };
}
struct VarBinding {
name: String,
value: String,
}
thread_local! {
static VARS: RefCell<Vec<VarBinding>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
pub fn enter_var(name: String, value: String) -> VarGuard {
VARS.with(|v| v.borrow_mut().push(VarBinding { name, value }));
VarGuard
}
pub struct VarGuard;
impl Drop for VarGuard {
fn drop(&mut self) {
VARS.with(|v| {
v.borrow_mut().pop();
});
}
}
#[must_use]
pub fn enter_loop_var(name: String, read_repr: String, write_repr: String) -> LoopGuard {
LOOP_VARS.with(|v| v.borrow_mut().push(LoopVar { name, read_repr, write_repr }));
LoopGuard
}
pub struct LoopGuard;
impl Drop for LoopGuard {
fn drop(&mut self) {
LOOP_VARS.with(|v| {
v.borrow_mut().pop();
});
}
}
thread_local! {
static STDIN_REPR: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
pub fn enter_stdin_repr(repr: String) -> StdinReprGuard {
STDIN_REPR.with(|v| v.borrow_mut().push(repr));
StdinReprGuard
}
pub fn stdin_item_repr() -> Option<String> {
STDIN_REPR.with(|v| v.borrow().last().cloned())
}
pub struct StdinReprGuard;
impl Drop for StdinReprGuard {
fn drop(&mut self) {
STDIN_REPR.with(|v| {
v.borrow_mut().pop();
});
}
}
pub fn expand_vars(path: &str, want_write: bool) -> Cow<'_, str> {
if !path.contains('$') {
return Cow::Borrowed(path);
}
let replaced = LOOP_VARS.with(|lv| {
VARS.with(|v| {
let loops = lv.borrow();
let vars = v.borrow();
if loops.is_empty() && vars.is_empty() {
None
} else {
expand_with(path, &loops, &vars, want_write)
}
})
});
replaced.map_or(Cow::Borrowed(path), Cow::Owned)
}
fn expand_with(path: &str, loops: &[LoopVar], vars: &[VarBinding], want_write: bool) -> Option<String> {
let mut out = String::with_capacity(path.len());
let mut rest = path;
let mut replaced = false;
while let Some(dollar) = rest.find('$') {
out.push_str(&rest[..dollar]);
let after = &rest[dollar + 1..];
match parse_var(after) {
Some((name, consumed)) => {
if let Some(lv) = loops.iter().rev().find(|v| v.name == name) {
out.push_str(if want_write { &lv.write_repr } else { &lv.read_repr });
replaced = true;
} else if let Some(vb) = vars.iter().rev().find(|v| v.name == name) {
out.push_str(&vb.value);
replaced = true;
} else {
out.push('$');
out.push_str(&after[..consumed]);
}
rest = &after[consumed..];
}
None => {
out.push('$');
rest = after;
}
}
}
out.push_str(rest);
replaced.then_some(out)
}
fn parse_var(after: &str) -> Option<(&str, usize)> {
if let Some(braced) = after.strip_prefix('{') {
let close = braced.find('}')?;
let name = &braced[..close];
is_var_name(name).then_some((name, close + 2)) } else if after.as_bytes().first().is_some_and(u8::is_ascii_digit) {
Some((&after[..1], 1)) } else {
let len = after.bytes().take_while(|&b| b.is_ascii_alphanumeric() || b == b'_').count();
let name = &after[..len];
is_var_name(name).then_some((name, len))
}
}
fn is_var_name(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.bytes().all(|b| b.is_ascii_digit()) {
return true;
}
let mut bytes = s.bytes();
matches!(bytes.next(), Some(b) if b.is_ascii_alphabetic() || b == b'_')
&& bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
pub fn resolve(path: &str) -> Cow<'_, str> {
if path.is_empty() || path.starts_with('~') || path.contains('$') {
return Cow::Borrowed(path);
}
let resolved = CURRENT.with(|c| {
let ctx = c.borrow();
match (ctx.cwd.as_deref(), ctx.root.as_deref()) {
(Some(cwd), Some(root)) if cwd.starts_with('/') && root.starts_with('/') => {
let abs = if path.starts_with('/') {
lexical_join("/", path)
} else {
lexical_join(cwd, path)
};
Some(express_relative_to_root(&abs, root))
}
_ => None,
}
});
resolved.map_or(Cow::Borrowed(path), Cow::Owned)
}
pub fn join_cwd(cur: Option<&str>, target: &str) -> Option<String> {
if target.starts_with('~') || target.contains('$') {
return None; }
if target.starts_with('/') {
return Some(lexical_join("/", target)); }
cur.filter(|c| c.starts_with('/')).map(|c| lexical_join(c, target)) }
fn express_relative_to_root(abs: &str, root: &str) -> String {
let root = root.trim_end_matches('/');
if abs == root {
return ".".to_string(); }
match abs.strip_prefix(root) {
Some(inside) if inside.starts_with('/') => inside.trim_start_matches('/').to_string(),
_ => abs.to_string(),
}
}
fn lexical_join(base: &str, rel: &str) -> String {
let mut parts: Vec<&str> = base.split('/').filter(|s| !s.is_empty()).collect();
for seg in rel.split('/') {
match seg {
"" | "." => {}
".." => {
parts.pop();
}
s => parts.push(s),
}
}
format!("/{}", parts.join("/"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_context_leaves_paths_unchanged() {
assert_eq!(resolve("./x"), "./x");
assert_eq!(resolve("config"), "config");
assert_eq!(resolve("/etc/x"), "/etc/x");
}
#[test]
fn relative_inside_the_project_stays_worktree_relative() {
let _g = enter(PathCtx { cwd: Some("/home/u/proj/sub".into()), root: Some("/home/u/proj".into()) });
assert_eq!(resolve("x"), "sub/x", "cwd under root → root-relative");
assert_eq!(resolve("./y"), "sub/y");
assert_eq!(resolve("../z"), "z", ".. that stays inside root");
}
#[test]
fn relative_outside_the_project_becomes_absolute() {
let _g = enter(PathCtx { cwd: Some("/etc".into()), root: Some("/home/u/proj".into()) });
assert_eq!(resolve("x"), "/etc/x", "cd /etc → the real target");
assert_eq!(resolve("passwd"), "/etc/passwd");
assert_eq!(resolve("*"), "/etc/*");
}
#[test]
fn dotdot_escaping_the_project_becomes_absolute() {
let _g = enter(PathCtx { cwd: Some("/home/u/proj".into()), root: Some("/home/u/proj".into()) });
assert_eq!(resolve("../../../etc/x"), "/etc/x");
}
#[test]
fn absolute_in_root_becomes_root_relative_outside_stays_absolute() {
let _g = enter(PathCtx { cwd: Some("/home/u/proj/sub".into()), root: Some("/home/u/proj".into()) });
assert_eq!(resolve("/home/u/proj/main.rs"), "main.rs");
assert_eq!(resolve("/home/u/proj/sub/x"), "sub/x");
assert_eq!(resolve("/home/u/proj/a/../b"), "b", "normalized in place");
assert_eq!(resolve("/home/u/proj"), ".", "the project root itself");
assert_eq!(resolve("/usr/bin/x"), "/usr/bin/x");
assert_eq!(resolve("/home/u/proj/../../etc/x"), "/home/etc/x", "climbs to /home, still outside root");
assert_eq!(resolve("/home/u/proj/../../../etc/x"), "/etc/x", "escapes to /etc via ..");
assert_eq!(
resolve("/home/u/proj-evil/secret"), "/home/u/proj-evil/secret",
"a sibling dir is not confused for inside by bare string prefix",
);
assert_eq!(resolve("$HOME/x"), "$HOME/x");
assert_eq!(resolve("~/x"), "~/x");
}
#[test]
fn loop_var_expands_to_its_representative_per_face() {
let _g = enter_loop_var("f".into(), "read_item".into(), "write_item".into());
assert_eq!(expand_vars("$f", false), "read_item");
assert_eq!(expand_vars("$f", true), "write_item");
assert_eq!(expand_vars("${f}", false), "read_item");
assert_eq!(expand_vars("$f.bak", false), "read_item.bak", "compound suffix");
assert_eq!(expand_vars("pre/$f", false), "pre/read_item");
assert_eq!(expand_vars("$foo", false), "$foo", "$foo is not $f");
assert_eq!(expand_vars("$g", false), "$g", "unbound var untouched");
assert_eq!(expand_vars("plain", false), "plain");
}
#[test]
fn loop_var_binding_is_scoped_and_nests() {
assert_eq!(expand_vars("$f", false), "$f", "no binding");
{
let _outer = enter_loop_var("f".into(), "outer".into(), "outer".into());
{
let _inner = enter_loop_var("f".into(), "inner".into(), "inner".into());
assert_eq!(expand_vars("$f", false), "inner", "innermost wins");
}
assert_eq!(expand_vars("$f", false), "outer", "inner popped on drop");
}
assert_eq!(expand_vars("$f", false), "$f", "all popped");
}
#[test]
fn the_guard_restores_on_drop() {
{
let _g = enter(PathCtx { cwd: Some("/etc".into()), root: Some("/r".into()) });
assert_eq!(resolve("x"), "/etc/x");
}
assert_eq!(resolve("x"), "x", "context cleared after the guard drops");
}
}