clockworklabs/SpacetimeDB · error · io::Error

cannot replace without enclosing directory

Error message

cannot replace {} without enclosing directory

What it means

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.

Solutions

  1. Validate the path is non-empty and has a file name before calling write (assert path.file_name() is Some)
  2. Fix the source of the empty path: default the config value to a real directory-qualified location like a subdirectory plus file name
  3. Log the offending path at the configuration boundary so the bad value is visible

Example fix

// before
let p: PathBuf = std::env::var("STATE_FILE").unwrap_or_default().into();
p.write(b"{}")?; // InvalidInput: cannot replace  without enclosing directory

// after
let raw = std::env::var("STATE_FILE").unwrap_or_default();
anyhow::ensure!(!raw.is_empty(), "STATE_FILE must be set to a file path");
let p = PathBuf::from(raw);
anyhow::ensure!(p.file_name().is_some(), "STATE_FILE must name a file, got {raw:?}");
p.write(b"{}")?;
Defensive patterns

Strategy: validation

Validate before calling

fn writable_target(p: &Path) -> anyhow::Result<()> {
    anyhow::ensure!(!p.as_os_str().is_empty(), "path must not be empty");
    anyhow::ensure!(p.file_name().is_some(), "path must name a file, got {}", p.display());
    Ok(())
}

Type guard

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

Try / catch

if let Err(e) = path.write(contents) {
    if e.kind() == std::io::ErrorKind::InvalidInput
        && e.to_string().contains("without enclosing directory") {
        return Err(anyhow::anyhow!("config produced invalid path {}", path.display()));
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/b9fde14219e1531b. Report an issue: GitHub.

Appendix: source

Thrown at crates/paths/src/utils.rs:111

        }
    };
    ($(#[$($attr:tt)+])* $name:ident: file) => {
        path_type!($(#[$($attr)+])* $name);
        impl $name {
            pub fn read(&self) -> std::io::Result<Vec<u8>> {
                std::fs::read(self)
            }

            pub fn read_to_string(&self) -> std::io::Result<String> {
                std::fs::read_to_string(self)
            }

            pub fn write(&self, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
                use std::io::Write as _;

                let path = &self.0;
                let parent = path.parent().ok_or_else(||
                    std::io::Error::new(
                        std::io::ErrorKind::InvalidInput,
                        format!("cannot replace {} without enclosing directory", path.display()))
                )?;
                std::fs::create_dir_all(&parent)?;

                let mut tmp = $crate::__tempfile::NamedTempFile::new_in(parent)?;
                tmp.write_all(contents.as_ref())?;
                tmp.as_file().sync_all()?;
                tmp.persist(&path)?;
                // On Windows, syncing the directory is not necessary and doesn't even work.
                #[cfg(not(target_os = "windows"))]
                std::fs::File::open(parent)?.sync_all()?;

                Ok(())
            }

            /// Opens a file at this path with the given options, ensuring its parent directory exists.
            #[inline]

View on GitHub (pinned to 6dee26c6ef)