Skip to main content

vtcode_core/tools/validation/
unified_path.rs

1use anyhow::{Result, bail};
2use std::path::{Path, PathBuf};
3
4/// Consolidated path validation combining safety checks, normalization, and workspace bounds.
5///
6/// Steps:
7/// 1. Shell character + system path safety (from validation/paths.rs)
8/// 2. Normalize + workspace bounds check (from traits.rs)
9/// 3. Canonical resolve + re-check for symlink escapes (from path_policy.rs)
10pub async fn validate_and_resolve_path(workspace_root: &Path, path_str: &str) -> Result<PathBuf> {
11    // Step 1: Safety checks (dangerous chars, system paths, traversal)
12    super::paths::validate_path_safety(path_str)?;
13
14    // Step 2: Normalize and check workspace bounds
15    let path = Path::new(path_str);
16    let absolute = if path.is_absolute() {
17        path.to_path_buf()
18    } else {
19        workspace_root.join(path)
20    };
21    let normalized = crate::utils::path::normalize_path(&absolute);
22    let normalized_root = crate::utils::path::normalize_path(workspace_root);
23
24    if !normalized.starts_with(&normalized_root) {
25        bail!(
26            "Path '{}' resolves outside the workspace boundary",
27            path_str
28        );
29    }
30
31    // Step 3: Canonical resolve + re-check (catches symlink escapes)
32    let canonical = crate::utils::path::canonicalize_allow_missing(&normalized).await?;
33    let canonical_root = if tokio::fs::try_exists(workspace_root).await.unwrap_or(false) {
34        tokio::fs::canonicalize(workspace_root)
35            .await
36            .unwrap_or_else(|_| normalized_root.clone())
37    } else {
38        normalized_root
39    };
40
41    if !canonical.starts_with(&canonical_root) {
42        bail!(
43            "Path '{}' resolves outside the workspace boundary (after symlink resolution)",
44            path_str
45        );
46    }
47
48    Ok(canonical)
49}