Hmbown/CodeWhale · error · anyhow::Error

path has no parent

Error message

path has no parent

What it means

write_atomic in crates/tui/src/integrations/dsh/receipt.rs writes receipts via a sibling temp file plus rename so a crash never leaves a torn file. It calls Path::parent() on the destination to place the temp file, and Path::parent() returns None when the path has no directory component (a bare file name, an empty path, or the root). In that case the write fails with 'path has no parent' before any I/O happens.

Source

Thrown at crates/tui/src/integrations/dsh/receipt.rs:145

        }
    }

    pub(crate) fn save(&self, path: &Path) -> Result<()> {
        let parent = path
            .parent()
            .ok_or_else(|| anyhow::anyhow!("receipt path has no parent"))?;
        std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
        let json = serde_json::to_vec_pretty(self)?;
        write_atomic(path, &json)
    }
}

/// Write via a sibling temp file + rename so a crash never leaves a torn
/// receipt or overlay behind.
pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
    let parent = path
        .parent()
        .ok_or_else(|| anyhow::anyhow!("path has no parent"))?;
    let tmp = parent.join(format!(
        ".{}.tmp-{}",
        path.file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| "file".to_string()),
        std::process::id()
    ));
    std::fs::write(&tmp, bytes).with_context(|| format!("write {}", tmp.display()))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600));
    }
    std::fs::rename(&tmp, path).with_context(|| format!("rename into {}", path.display()))?;
    Ok(())
}

pub(crate) fn now_rfc3339() -> String {

View on GitHub (pinned to 8880682c63)

Solutions

  1. Build the destination as dir.join("receipt.json") so the path always has a directory component
  2. If the path comes from configuration, canonicalize or join it against the receipts directory before calling save
  3. At CLI/config boundaries, reject user-supplied paths that are bare file names with a clear message

Example fix

// before
let path = PathBuf::from("receipt.json");
receipt.save(&path)?;

// after
let path = receipts_dir.join("receipt.json");
receipt.save(&path)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_parented(path: &std::path::Path) -> anyhow::Result<()> {
    match path.parent() {
        Some(parent) if !parent.as_os_str().is_empty() => Ok(()),
        _ => anyhow::bail!(
            "path '{}' must include a directory component",
            path.display()
        ),
    }
}
// call before receipt.save(path) or write_atomic(path, bytes)
ensure_parented(&path)?;

Type guard

fn has_directory_component(path: &std::path::Path) -> bool {
    path.parent()
        .map(|p| !p.as_os_str().is_empty())
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling Receipt::save or write_atomic with a relative bare file name like "receipt.json", with Path::new(""), or with a root path. The parent lookup fails immediately; no temp file is created.

Common situations: A caller builds the receipt path from a config value that holds only a file name; a receipts directory default is empty so nothing is joined onto the file name; unit tests pass a bare relative path.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/0972a585f2c9331c. Report an issue: GitHub.