flxzt/rnote · error

The filepath does not have a parent directory

Error message

The filepath does not have a parent directory

What it means

Thrown by atomic_save_to_file in rnote-engine utils when Path::parent() returns None for the given filepath. In Rust this happens only for paths with no parent component, i.e. the root path "/" (or effectively empty paths), for which no directory exists in which to create the atomic temporary file.

Solutions

  1. Validate the path has a parent directory before calling atomic_save_to_file
  2. Reject root/empty paths at the API boundary with a user-facing error
  3. Join the file name to a known directory instead of passing a bare path
  4. Use filepath.has_parent()-style checks via filepath.parent().is_some()

Example fix

// before
atomic_save_to_file("/", bytes)?;
// after
let path = Path::new("/tmp").join("file.dat");
assert!(path.parent().is_some());
atomic_save_to_file(&path, bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

if filepath.parent().is_none() {
    anyhow::bail!("path {:?} has no parent directory", filepath);
}
if !filepath.parent().unwrap().is_dir() {
    std::fs::create_dir_all(filepath.parent().unwrap())?;
}

Type guard

fn has_parent(p: &std::path::Path) -> bool { p.parent().is_some() && p.file_name().is_some() }

Try / catch

match atomic_save_to_file(&path, bytes) {
    Ok(()) => {},
    Err(e) if e.to_string().contains("parent directory") => eprintln!("invalid save path: {}", path.display()),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a filepath that is the filesystem root "/" (or a bare path whose parent() is None) to atomic_save_to_file.

Common situations: Building a save path from a config value that is empty or reduced to "/", unvalidated user-supplied paths, or path-joining bugs that strip the directory component.

Related errors


AI-assisted analysis of flxzt/rnote@bbc5354502 (2026-09-08). Data as JSON: /api/errors/33930fb7b90158b6. Report an issue: GitHub.

Appendix: source

Thrown at crates/rnote-engine/src/utils.rs:135

    /// Deserialize base64 encoded [glib::Bytes]
    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<glib::Bytes, D::Error> {
        rnote_compose::serialize::sliceu8_base64::deserialize(d).map(glib::Bytes::from_owned)
    }
}

/// Attempts to atomically save data to a file.
/// Not asynchronous, wrap with `blocking::unblock()` or equivalent to avoid blocking.
pub fn atomic_save_to_file<Q, B>(filepath: Q, bytes: B) -> anyhow::Result<()>
where
    Q: AsRef<std::path::Path>,
    B: AsRef<[u8]>,
{
    let filepath = filepath.as_ref();
    let bytes = bytes.as_ref();

    let parent_directory = filepath
        .parent()
        .ok_or_else(|| anyhow::anyhow!("The filepath does not have a parent directory"))?;

    // We first create the named temporary file, specifically in the parent
    // directory of the target filepath, as `.persist()` will not work
    // if the temporary file is in a different filesystem than the target.
    let mut temp_file = tempfile::NamedTempFile::new_in(parent_directory)
        .with_context(|| "Failed to create a temporary file")?;

    // We then write all of our bytes to the temporary file before syncing its contents.
    temp_file
        .write_all(bytes)
        .with_context(|| "Failed to write to the temporary file")?;
    temp_file
        .as_file()
        .sync_all()
        .with_context(|| "Failed to sync the contents and metadata of the temporary file")?;

    // Finally, we persist the temporary file to the target filepath, if a file
    // preexists at this location, it will be atomically replaced by our new file.

View on GitHub (pinned to bbc5354502)