Hmbown/CodeWhale · error

must be a single path component

Error message

{kind} must be a single path component

What it means

ensure_safe_storage_id rejects values that are not exactly one normal path component — containing '/', '\\', '..' or similar — to prevent path traversal or nested storage paths. It throws '{kind} must be a single path component' when the value has multiple components or a non-Normal component (RootDir, CurDir, ParentDir, Prefix).

Solutions

  1. Sanitize the id: strip or replace path separators and reject '..' before calling the API (see sanitize_filename in the same file).
  2. Generate ids internally (UUID/ULID) instead of accepting raw external strings.
  3. Keep this rejection — do not work around it; it is a deliberate path-traversal guard.

Example fix

// before
manager.store_task(&user_input, task).await?; // user_input = "../evil"
// after
let safe = sanitize_filename(&user_input);
manager.store_task(&safe, task).await?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_id(value: &str) -> bool {
    !value.is_empty()
        && !value.contains(['/', '\\'])
        && value != "." && value != ".."
}

Prevention

When it happens

Trigger: Passing an id containing a path separator or '..' (e.g. "../escape", "a/b", "C:\\x") into a storage-backed task-manager call.

Common situations: User-supplied ids echoed into storage keys; unsanitized external input; constructing ids by joining strings with '/' by mistake; attempted path-traversal via crafted task ids.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/abd0df50bce8db47. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/task_manager.rs:3391

            out.push_str("...");
            return out;
        }
        if ch.is_control() && ch != '\n' && ch != '\t' {
            continue;
        }
        out.push(ch);
        count += 1;
    }
    out
}

fn ensure_safe_storage_id(kind: &str, value: &str) -> Result<()> {
    let mut components = Path::new(value).components();
    let Some(component) = components.next() else {
        bail!("{kind} must not be empty");
    };
    if components.next().is_some() || !matches!(component, std::path::Component::Normal(_)) {
        bail!("{kind} must be a single path component");
    }
    Ok(())
}

fn sanitize_filename(input: &str) -> String {
    let mut out = String::new();
    for ch in input.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
            out.push(ch);
        } else {
            out.push('_');
        }
    }
    if out.is_empty() {
        "artifact".to_string()
    } else {
        out
    }

View on GitHub (pinned to 73e0f67d83)