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
- Build the destination as dir.join("receipt.json") so the path always has a directory component
- If the path comes from configuration, canonicalize or join it against the receipts directory before calling save
- 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
- Always build receipt paths with dir.join(file_name), never a bare file name
- Prefer absolute paths for receipt writes
- Reject user-supplied bare file names at the CLI boundary with a clear message
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
- current Codewhale executable path is not valid UTF-8: {}
- resolving external credential path: {err}
- invalid Codewhale-owned xAI OAuth basename
- xAI OAuth private basename must be one UTF-8 path component
- xAI OAuth credentials directory must be absolute
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/0972a585f2c9331c.
Report an issue: GitHub.