flxzt/rnote · error · anyhow::Error

file of source path ' ' does not have a file stem.

Error message

file of source path '{adjusted_source_path:?}' does not have a file stem.

What it means

This error is thrown by generate_destination_path when the adjusted source path has no file stem. Path::file_stem() returns None for paths that end in '..' or '/', or for paths whose final component is empty or dot-only (e.g. '.', '..'), or for an empty path. Since the duplicate operation needs a stem to build 'name (copy 2).ext', it cannot proceed and errors out.

Solutions

  1. Validate the source path before calling duplicate: ensure it is non-empty, has a file_name()/file_stem(), and points to an existing file or directory in the workspace.
  2. Check that remove_dup_suffix cannot reduce the filename to nothing; if the whole stem matched the duplicate suffix pattern, fall back to the original name instead of the stripped one.
  3. Guard the workspace-browser UI so Duplicate is only enabled for rows backed by a valid file path (disable action when the row's path is empty).
  4. When the error occurs, skip the row and log it rather than aborting the whole batch of duplicates.

Example fix

// before
let dest = generate_destination_path(&entry.path)?;
// after
if entry.path.as_os_str().is_empty() || entry.path.file_stem().is_none() {
    anyhow::bail!("skipping duplicate: '{}' has no valid file name", entry.path.display());
}
let dest = generate_destination_path(&entry.path)?;
Defensive patterns

Strategy: validation

Validate before calling

fn validate_duplicate_source(source: &std::path::Path) -> anyhow::Result<()> {
    if source.as_os_str().is_empty() {
        anyhow::bail!("source path is empty");
    }
    if source.file_stem().is_none() {
        anyhow::bail!("source path '{}' has no file stem", source.display());
    }
    Ok(())
}

Type guard

fn has_file_stem(source: &std::path::Path) -> bool {
    source.file_stem().map(|s| !s.is_empty()).unwrap_or(false)
}

Try / catch

match generate_destination_path(&source) {
    Ok(dest) => duplicate_at(dest),
    Err(e) if e.to_string().contains("does not have a file stem") => {
        log::warn!("skipping invalid duplicate source: {e}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling duplicate_file or duplicate_dir with a source path whose final component has no stem after remove_dup_suffix — typically an empty path, a path ending in '/', '.', or '..', or a path whose name consisted solely of duplicate suffix text that remove_dup_suffix stripped away.

Common situations: User triggers Duplicate in the workspace browser on a malformed/blank row entry (workspace entry with empty relative path), a path built from unvalidated user input or a stale index row, or an edge-case file literally named '.' or '..' after suffix removal.

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 flxzt/rnote@bbc5354502 (2026-09-08). Data as JSON: /api/errors/8d4c30f4249ac9e3. Report an issue: GitHub.

Appendix: source

Thrown at crates/rnote-ui/src/workspacebrowser/filerow/actions/duplicate.rs:103

        &[source.as_ref()],
        destination,
        &fs_extra::dir::CopyOptions {
            copy_inside: true,
            ..Default::default()
        },
    )?;
    Ok(())
}

/// returns a suitable not-already-existing destination path from the given source path
/// by adding or replacing `<delim><num>` to the source-path, where `<num>` is incremented as often as needed.
fn generate_destination_path(source: impl AsRef<Path>) -> anyhow::Result<PathBuf> {
    let mut duplicate_index = 1;
    let mut destination_path = source.as_ref().to_owned();
    let adjusted_source_path = remove_dup_suffix(source);

    let Some(source_stem) = adjusted_source_path.file_stem() else {
        return Err(anyhow::anyhow!(
            "file of source path '{adjusted_source_path:?}' does not have a file stem."
        ));
    };

    let source_extension = adjusted_source_path.extension();

    // Loop to find the next available duplicate filename
    loop {
        destination_path.set_file_name(generate_duplicate_filename(
            source_stem,
            source_extension,
            duplicate_index,
        ));

        if !destination_path.exists() {
            return Ok(destination_path);
        }

View on GitHub (pinned to bbc5354502)