{"record":{"id":"b9fde14219e1531b","repo":"clockworklabs/SpacetimeDB","slug":"cannot-replace-without-enclosing-directory","errorCode":null,"errorMessage":"cannot replace {} without enclosing directory","messagePattern":"cannot replace (.+?) without enclosing directory","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/paths/src/utils.rs","lineNumber":111,"sourceCode":"        }\n    };\n    ($(#[$($attr:tt)+])* $name:ident: file) => {\n        path_type!($(#[$($attr)+])* $name);\n        impl $name {\n            pub fn read(&self) -> std::io::Result<Vec<u8>> {\n                std::fs::read(self)\n            }\n\n            pub fn read_to_string(&self) -> std::io::Result<String> {\n                std::fs::read_to_string(self)\n            }\n\n            pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> {\n                use std::io::Write as _;\n\n                let path = &self.0;\n                let parent = path.parent().ok_or_else(||\n                    std::io::Error::new(\n                        std::io::ErrorKind::InvalidInput,\n                        format!(\"cannot replace {} without enclosing directory\", path.display()))\n                )?;\n                std::fs::create_dir_all(&parent)?;\n\n                let mut tmp = $crate::__tempfile::NamedTempFile::new_in(parent)?;\n                tmp.write_all(contents.as_ref())?;\n                tmp.as_file().sync_all()?;\n                tmp.persist(&path)?;\n                // On Windows, syncing the directory is not necessary and doesn't even work.\n                #[cfg(not(target_os = \"windows\"))]\n                std::fs::File::open(parent)?.sync_all()?;\n\n                Ok(())\n            }\n\n            /// Opens a file at this path with the given options, ensuring its parent directory exists.\n            #[inline]","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/paths/src/utils.rs#L93-L129","documentation":"crates/paths provides a macro-generated Path wrapper whose write() does atomic replace: it requires an enclosing directory to place the temp file (NamedTempFile::new_in(parent)) and to fsync the directory afterwards. It calls path.parent() and fails with ErrorKind::InvalidInput, 'cannot replace {path} without enclosing directory', when the path has no parent component. Per std::path semantics, only the empty path \"\" and the root \"/\" have no parent.","triggerScenarios":"Calling .write(...) on a Path constructed from an empty string; writing to the filesystem root \"/\"; building a path from unvalidated/unset config (e.g. empty env var) that joins to \"\"; a component-joining bug that produced an empty path string.","commonSituations":"Config-driven file locations where a variable is missing and defaults to \"\" (unset HOME, empty state_dir); CLI tools writing to a user-supplied path that turns out empty; path manipulation that strips all components.","solutions":["Validate the path is non-empty and has a file name before calling write (assert path.file_name() is Some)","Fix the source of the empty path: default the config value to a real directory-qualified location like a subdirectory plus file name","Log the offending path at the configuration boundary so the bad value is visible"],"exampleFix":"// before\nlet p: PathBuf = std::env::var(\"STATE_FILE\").unwrap_or_default().into();\np.write(b\"{}\")?; // InvalidInput: cannot replace  without enclosing directory\n\n// after\nlet raw = std::env::var(\"STATE_FILE\").unwrap_or_default();\nanyhow::ensure!(!raw.is_empty(), \"STATE_FILE must be set to a file path\");\nlet p = PathBuf::from(raw);\nanyhow::ensure!(p.file_name().is_some(), \"STATE_FILE must name a file, got {raw:?}\");\np.write(b\"{}\")?;","handlingStrategy":"validation","validationCode":"fn writable_target(p: &Path) -> anyhow::Result<()> {\n    anyhow::ensure!(!p.as_os_str().is_empty(), \"path must not be empty\");\n    anyhow::ensure!(p.file_name().is_some(), \"path must name a file, got {}\", p.display());\n    Ok(())\n}","typeGuard":"fn has_enclosing_dir(p: &Path) -> bool { p.parent().map(|d| !d.as_os_str().is_empty()).unwrap_or(false) }","tryCatchPattern":"if let Err(e) = path.write(contents) {\n    if e.kind() == std::io::ErrorKind::InvalidInput\n        && e.to_string().contains(\"without enclosing directory\") {\n        return Err(anyhow::anyhow!(\"config produced invalid path {}\", path.display()));\n    }\n    return Err(e.into());\n}","preventionTips":["Validate config-supplied paths (non-empty, has file name) at load time","Default every config path to a directory-qualified value, never \"\"","Fail fast on unset env vars instead of defaulting them to empty strings"],"tags":["filesystem","path","rust","config","atomic-write"],"backgroundTag":"invalid-file-path","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}