clockworklabs/SpacetimeDB · error · io::Error

cannot replace {} without enclosing directory

Error message

cannot replace {} without enclosing directory

What it means

The paths crate's macro-generated atomic write helper stages a NamedTempFile in the target's parent directory and persists it over the destination. Path::parent() returned None, which in Rust happens only for filesystem roots such as / (or a Windows drive root): there is no enclosing directory to stage the temp file in, so the write is rejected with InvalidInput before touching the filesystem.

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 524b4487d9)

Solutions

  1. Fix the base-path construction so an empty or root prefix can never produce a root path.
  2. Validate that the target path has a real parent component and is not a root before writing.
  3. Reject empty/root paths at the API boundary with a clear error message.

Example fix

// before: prefix from config can resolve to the root
let path = PathBuf::from(&prefix).join(&name);
path.write(contents)?; // InvalidInput when path == "/"

// after: reject root paths explicitly
let path = PathBuf::from(&prefix).join(&name);
if path.parent().is_none() {
    return Err(format!("refusing to write root path {}", path.display()));
}
path.write(contents)?;
Defensive patterns

Strategy: type-guard

Validate before calling

use std::path::Path;

fn ensure_writable(path: &Path) -> Result<(), String> {
    match path.parent() {
        Some(p) if !p.as_os_str().is_empty() => Ok(()),
        _ => Err(format!("path {} has no enclosing directory", path.display())),
    }
}

Type guard

fn is_root_path(p: &std::path::Path) -> bool {
    p.parent().is_none()
}

Try / catch

match path.write(contents) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {
        // Path construction bug: log the offending path and fix the base/prefix logic.
    }
    r => r,
}

Prevention

When it happens

Trigger: A path that normalizes to the filesystem root - PathBuf::from("/"), joining onto a misconfigured empty/root base, or collecting an empty component iterator - passed to the generated write method.

Common situations: Config or argument parsing where an empty prefix, tenant, or component concatenates into a root path; user-supplied path strings used unvalidated; scripts accidentally writing to /.

Related errors


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