astrid-runtime/astrid · error · io::Error

layout record has no file name

Error message

layout record has no file name

What it means

atomic_write needs the file name component of the target path to build its staging file (.{name}.next). When path.file_name() is None or is not valid UTF-8 (e.g. the path is "..", ends in "..", or contains non-UTF-8 bytes on unix), it raises InvalidInput "layout record has no file name". This guards against silently mis-staging the atomic write.

Source

Thrown at crates/astrid-core/src/dirs_layout.rs:525

fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
    #[cfg(windows)]
    {
        crate::platform_fs::atomic_write_private_file(path, bytes)
    }

    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;

        let parent = path.parent().ok_or_else(|| {
            io::Error::new(io::ErrorKind::InvalidInput, "layout record has no parent")
        })?;
        std::fs::create_dir_all(parent)?;
        let name = path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "layout record has no file name",
                )
            })?;
        let staged = parent.join(format!(".{name}.next"));
        match std::fs::symlink_metadata(&staged) {
            Ok(metadata) if metadata.file_type().is_file() => std::fs::remove_file(&staged)?,
            Ok(_) => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("layout staging path is redirected: {}", staged.display()),
                ));
            },
            Err(error) if error.kind() == io::ErrorKind::NotFound => {},
            Err(error) => return Err(error),
        }
        let mut file = OpenOptions::new()
            .write(true)

View on GitHub (pinned to affd8760f4)

Solutions

  1. Pass a path with a concrete UTF-8 file name, e.g. layout_dir.join("layout-v2.json").
  2. Check path.file_name().and_then(OsStr::to_str).is_some() before calling.
  3. Reject paths ending in ".." or "/" in your own path-building code.
  4. Convert non-UTF-8 sources to valid UTF-8 names when constructing the record path.

Example fix

// before
let path = layout_dir.join("..");
write_layout_version(&path, &record)?;
// after
let path = layout_dir.join("layout-v2.json");
write_layout_version(&path, &record)?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_valid_file_name(path: &Path) -> bool {
    path.file_name().and_then(|n| n.to_str()).is_some()
}

Type guard

fn utf8_file_name(path: &Path) -> Option<&str> {
    path.file_name().and_then(|n| n.to_str())
}

Try / catch

match write_layout_version(path, record) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput && e.to_string().contains("no file name") => {
        eprintln!("path must end in a concrete UTF-8 file name: {}", path.display());
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling write_layout_version with a path that is a directory root, "..", ends with a separator, or whose final component is non-UTF-8, so and_then(|name| name.to_str()) yields None.

Common situations: Passing a directory instead of a file path; paths built from raw OS bytes with non-UTF-8 components; accidentally joining an empty file-name onto the layout directory.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/e92d4b9b36ed8e89. Report an issue: GitHub.