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
- 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.
- 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.
- 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).
- 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
- Validate workspace entries at load time so rows never carry empty or malformed relative paths
- Only enable the Duplicate action when the row has a non-empty, stem-bearing file name
- After any name transformation (e.g. remove_dup_suffix), re-verify the result still yields a non-empty stem before building the destination
- Use Path::file_name()/file_stem() checks as a precondition instead of relying on the error to catch bad input
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
- Failed to get file stem
- Failed to get file name from output-file
- Failed to get file stem from rnote file
- Expected directory, found file
- Expected file, found directory
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)