{"record":{"id":"a607fb749c1bdad3","repo":"googleworkspace/cli","slug":"invalidinput","errorCode":"InvalidInput","errorMessage":"path has no parent directory","messagePattern":"path has no parent directory","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/google-workspace-cli/src/fs_util.rs","lineNumber":32,"sourceCode":"\n//! File-system utilities.\n\nuse std::io::{self, Write};\nuse std::path::Path;\n\n/// Write `data` to `path` atomically.\n///\n/// This implementation uses `tempfile::NamedTempFile` to create a temporary\n/// file with a random name, `O_EXCL` flags (preventing symlink attacks),\n/// and secure 0600 permissions from the moment of creation.\n///\n/// # Errors\n///\n/// Returns an `io::Error` if the temporary file cannot be created/written or if the\n/// final rename fails.\npub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {\n    let parent = path.parent().ok_or_else(|| {\n        io::Error::new(io::ErrorKind::InvalidInput, \"path has no parent directory\")\n    })?;\n\n    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;\n    tmp.write_all(data)?;\n    tmp.as_file().sync_all()?;\n    tmp.persist(path)\n        .map_err(|e| io::Error::new(e.error.kind(), e.error))?;\n\n    Ok(())\n}\n\n/// Async variant of [`atomic_write`] for use with tokio.\n///\n/// This implementation uses `create_new(true)` (O_EXCL) and `mode(0o600)` to\n/// prevent TOCTOU/symlink race conditions.\npub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<()> {\n    use rand::Rng;\n    use tokio::io::AsyncWriteExt;","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/googleworkspace/cli/blob/a3768d0e82ad83cca2da97724e46bea4ff0e6dbd/crates/google-workspace-cli/src/fs_util.rs#L14-L50","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet p = std::path::Path::new(\"\"); // degenerate path from empty cfg dir\nfs_util::atomic_write(p, &bytes)?;\n\n// after\nlet dir = std::env::var_os(\"CFG_DIR\").unwrap_or_else(|| \".\".into());\nlet p = std::path::Path::new(&dir).join(\"credentials.enc\");\nfs_util::atomic_write(&p, &bytes)?;","handlingStrategy":"validation","validationCode":"use std::path::Path;\n\nfn ensure_writable_file_path(p: &Path) -> std::io::Result<()> {\n    if p.as_os_str().is_empty() {\n        return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, \"empty target path\"));\n    }\n    if p.parent().is_none() {\n        return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, \"target must be a file under a directory, not a root\"));\n    }\n    Ok(())\n}\n\nensure_writable_file_path(&path)?;\nfs_util::atomic_write(&path, &bytes)?;","typeGuard":null,"tryCatchPattern":"if let Err(e) = fs_util::atomic_write(&path, &bytes) {\n    if e.kind() == std::io::ErrorKind::InvalidInput {\n        eprintln!(\"misconfigured target path '{path:?}': pass dir.join(\\\"file\\\") instead of a root/empty path\");\n    }\n}","preventionTips":["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"],"tags":["filesystem","path-validation","atomic-write","std-io"],"backgroundTag":"invalid-path-input","analyzedSha":"a3768d0e82ad83cca2da97724e46bea4ff0e6dbd","analyzedAt":"2026-08-16T19:51:46.516Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}