BloopAI/vibe-kanban · error · ContainerError

Failed to get relative path for {source_file:?}: {e}

Error message

Failed to get relative path for {source_file:?}: {e}

What it means

After the containment check passes, copy_single_file calls strip_prefix(source_root) to compute the file's path relative to the project root. If strip_prefix fails this error is thrown. Given the preceding starts_with(canonical_source) check, this should be nearly impossible, but it can still occur when source_file was constructed inconsistently (e.g. different separators or a non-normalized root).

Source

Thrown at crates/local-deployment/src/copy.rs:96

    source_root: &Path,
    target_root: &Path,
    seen: &mut HashSet<PathBuf>,
) -> Result<bool, ContainerError> {
    let canonical_source = source_root.canonicalize()?;
    let canonical_file = source_file.canonicalize()?;
    // Validate path is within source_dir
    if !canonical_file.starts_with(canonical_source) {
        return Err(ContainerError::Other(anyhow!(
            "File {source_file:?} is outside project directory"
        )));
    }

    if !seen.insert(canonical_file.clone()) {
        return Ok(false);
    }

    let relative_path = source_file.strip_prefix(source_root).map_err(|e| {
        ContainerError::Other(anyhow!(
            "Failed to get relative path for {source_file:?}: {e}"
        ))
    })?;

    let target_file = target_root.join(relative_path);

    if target_file.exists() {
        return Ok(false);
    }

    if let Some(parent) = target_file.parent()
        && !parent.exists()
    {
        fs::create_dir_all(parent)?;
    }
    fs::copy(source_file, &target_file)?;

    Ok(true)

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Normalize source_root before use (strip trailing separators, use the same PathBuf used to enumerate files).
  2. Compute relative paths from the canonicalized root (canonical_source) rather than the raw source_root.
  3. Log both source_root and source_file and verify they share the exact same prefix.
  4. Use pathdiff::diff_paths as a fallback when strip_prefix fails.

Example fix

// before
let relative_path = source_file.strip_prefix(source_root).map_err(...)?;
// after
let canonical_root = source_root.canonicalize()?;
let relative_path = canonical_file.strip_prefix(&canonical_root).map_err(...)?;
Defensive patterns

Strategy: validation

Validate before calling

let root = source_root.canonicalize()?;
if source_file.strip_prefix(&root).is_err() {
    return Err(anyhow!("{:?} not under canonical root {:?}", source_file, root));
}

Type guard

fn has_root_prefix(root: &Path, file: &Path) -> bool {
    file.starts_with(root)
}

Try / catch

match source_file.strip_prefix(source_root) {
    Ok(rel) => rel,
    Err(e) => {
        tracing::error!(file=?source_file, root=?source_root, "strip_prefix failed: {e}");
        return Err(e.into());
    }
}

Prevention

When it happens

Trigger: source_root passed with a trailing separator or non-normalized form ('./project/' vs 'project') while source_file was built from the normalized root, making strip_prefix fail despite containment passing via canonical paths.

Common situations: Programmatically joining paths with mixed separators on Windows; roots built with './' prefixes; callers passing symlinked root paths while files are enumerated from the canonical root.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/0c98191f3bd65df7. Report an issue: GitHub.