googleworkspace/cli · error · std::io::Error
InvalidInput
InvalidInput
Error message
path has no parent directory
What it means
atomic_write() rejects its target path with io::ErrorKind::InvalidInput when Path::parent() returns None. On Unix that only happens for the empty path "" and the filesystem root "/" (on Windows also for drive prefixes like "C:\"). The guard exists because tempfile::NamedTempFile::new_in(parent) needs a real directory to host the temporary file that is later atomically renamed over the target.
Source
Thrown at crates/google-workspace-cli/src/fs_util.rs:32
//! File-system utilities.
use std::io::{self, Write};
use std::path::Path;
/// Write `data` to `path` atomically.
///
/// This implementation uses `tempfile::NamedTempFile` to create a temporary
/// file with a random name, `O_EXCL` flags (preventing symlink attacks),
/// and secure 0600 permissions from the moment of creation.
///
/// # Errors
///
/// Returns an `io::Error` if the temporary file cannot be created/written or if the
/// final rename fails.
pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "path has no parent directory")
})?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
tmp.write_all(data)?;
tmp.as_file().sync_all()?;
tmp.persist(path)
.map_err(|e| io::Error::new(e.error.kind(), e.error))?;
Ok(())
}
/// Async variant of [`atomic_write`] for use with tokio.
///
/// This implementation uses `create_new(true)` (O_EXCL) and `mode(0o600)` to
/// prevent TOCTOU/symlink race conditions.
pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> {
use rand::Rng;
use tokio::io::AsyncWriteExt;View on GitHub (pinned to a3768d0e82)
Solutions
- Ensure the target is a concrete file path: build it as dir.join("filename") where dir comes from a validated config directory (e.g., dirs::config_dir() with a fallback), never pass a bare root or empty string
- Trace where the empty/root path originates — usually an env var (HOME, XDG_CONFIG_HOME) that is unset or empty — and default it explicitly
- Add a debug_assert!/early return in callers when path.as_os_str().is_empty() to catch regressions during development
Example fix
// before
let p = std::path::Path::new(""); // degenerate path from empty cfg dir
fs_util::atomic_write(p, &bytes)?;
// after
let dir = std::env::var_os("CFG_DIR").unwrap_or_else(|| ".".into());
let p = std::path::Path::new(&dir).join("credentials.enc");
fs_util::atomic_write(&p, &bytes)?; Defensive patterns
Strategy: validation
Validate before calling
use std::path::Path;
fn ensure_writable_file_path(p: &Path) -> std::io::Result<()> {
if p.as_os_str().is_empty() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "empty target path"));
}
if p.parent().is_none() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "target must be a file under a directory, not a root"));
}
Ok(())
}
ensure_writable_file_path(&path)?;
fs_util::atomic_write(&path, &bytes)?; Try / catch
if let Err(e) = fs_util::atomic_write(&path, &bytes) {
if e.kind() == std::io::ErrorKind::InvalidInput {
eprintln!("misconfigured target path '{path:?}': pass dir.join(\"file\") instead of a root/empty path");
}
} Prevention
- Always build destination paths as validated_dir.join("filename.ext") instead of assembling strings and calling Path::new on the result
- Default empty config-directory env vars (HOME, GOOGLE_WORKSPACE_CLI_CONFIG_DIR) explicitly instead of letting an empty value flow into path construction
- Add a unit test asserting atomic_write rejects "" and "/" so callers learn the contract from CI, not production
When it happens
Trigger: Calling atomic_write(Path::new(""), ...) after a config-dir variable came back empty; passing Path::new("/") because a join chain collapsed (e.g., an empty filename was joined onto the root); on Windows, passing a drive root like "C:\\"; any code that builds a path from user input without checking it is a full file path.
Common situations: HOME or GOOGLE_WORKSPACE_CLI_CONFIG_DIR resolution returned an empty string and the code joined it into a path that degenerated to the root; a path built as base.join("") where base is already "/"; unit tests passing a placeholder empty Path.
Related errors
- Failed to write client config: {e}
- Failed to create token directory '{}': {}
- Failed to set permissions on token directory '{}': {}
AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16).
Data as JSON: /api/errors/a607fb749c1bdad3.
Report an issue: GitHub.