Skip to main content

rusty_hooks/hook/
fs.rs

1use std::env;
2use std::path::{Path, PathBuf};
3
4/// Resolve the project root from (in priority order):
5/// 1. `CLAUDE_PROJECT_DIR` env var
6/// 2. `cwd` field in the hook payload
7/// 3. The process working directory
8pub fn project_root(cwd: Option<&str>) -> PathBuf {
9    if let Ok(val) = env::var("CLAUDE_PROJECT_DIR")
10        && !val.is_empty()
11    {
12        return canonicalize_or_raw(&val);
13    }
14    if let Some(c) = cwd.filter(|s| !s.is_empty()) {
15        return canonicalize_or_raw(c);
16    }
17    env::current_dir()
18        .and_then(|p| p.canonicalize())
19        .unwrap_or_else(|_| PathBuf::from("."))
20}
21
22/// Resolve `file_path` relative to `project_dir`.
23/// Returns `None` if the resolved path escapes the project root (traversal guard).
24pub fn resolve_file(project_dir: &Path, file_path: &str) -> Option<PathBuf> {
25    let project_dir = project_dir.canonicalize().ok()?;
26
27    let raw = Path::new(file_path);
28    let resolved = if raw.is_absolute() {
29        raw.to_path_buf()
30    } else {
31        project_dir.join(raw)
32    }
33    .canonicalize()
34    .ok()?;
35
36    resolved.starts_with(&project_dir).then_some(resolved)
37}
38
39/// Read a file to a `String`, returning `None` on any I/O error.
40pub fn read_text(path: &Path) -> Option<String> {
41    std::fs::read_to_string(path).ok()
42}
43
44fn canonicalize_or_raw(path: &str) -> PathBuf {
45    let p = PathBuf::from(path);
46    p.canonicalize().unwrap_or(p)
47}