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
- Fix the base-path construction so an empty or root prefix can never produce a root path.
- Validate that the target path has a real parent component and is not a root before writing.
- 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
- Validate user- or config-supplied paths at the API boundary before writing.
- Unit-test path-building code with empty prefixes and root bases.
- Treat root paths as programmer error and reject them loudly.
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
- Fatal Error: path {} exists but is not a directory.
- Fatal Error: path {} does not exist.
- Module bindings path {} exists but is not a directory.
- Path {} exists but is not a directory. A new SpacetimeDB pro
- AlreadyExists
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/b9fde14219e1531b.
Report an issue: GitHub.