libnyanpasu/clash-nyanpasu · error

destination path has no file name: {}

Error message

destination path has no file name: {}

What it means

`atomic_write` performs a crash-safe write by creating a temp file in a `.atomicwrite` directory under the target's parent and renaming it onto `path`. A rename/replace target must be an actual file name, so the guard rejects any path whose `file_name()` is None. This happens for paths that end in a separator, are a filesystem root, or are otherwise just a directory path.

Source

Thrown at backend/tauri/src/core/migration/fs.rs:21

//! Wraps [`atomicwrites`] (already a workspace dependency via `nyanpasu-core`)
//! behind a single helper so the migration store and every config rewrite share
//! one durable write path. Keeping the third-party type in one place means a
//! future swap only touches this file.

use anyhow::{Context, ensure};
use atomicwrites::{AllowOverwrite, AtomicFile};
use std::{io::Write, path::Path};

/// Atomically write `contents` to `path`.
///
/// The destination is never left half-written: `atomicwrites` writes the bytes
/// into a temp file under a randomized `.atomicwrite` subdirectory of the
/// target's parent, fsyncs it, then atomically replaces `path`. On Unix it also
/// fsyncs the parent directories so the rename survives a crash; on Windows it
/// replaces via `MoveFileExW` with write-through semantics. Missing parent
/// directories are created first.
pub(crate) fn atomic_write(path: &Path, contents: &[u8]) -> anyhow::Result<()> {
    ensure!(
        path.file_name().is_some(),
        "destination path has no file name: {}",
        path.display()
    );
    if let Some(parent) = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("failed to create dir {}", parent.display()))?;
    }
    AtomicFile::new(path, AllowOverwrite)
        .write(|file| file.write_all(contents))
        .with_context(|| format!("failed to atomically write {}", path.display()))?;
    Ok(())
}

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Append the intended file name to the target path before calling `atomic_write`
  2. Strip trailing separators from user- or env-supplied paths (use `PathBuf::set_extension` or push the file name explicitly)
  3. Check `path.file_name().is_some()` at the call site and log the full path to find where the path lost its last component

Example fix

// before
let dir = config_dir(); // e.g. "/home/u/.config/nyanpasu/"
atomic_write(Path::new(&dir), &bytes)?;
// after
let path = config_dir().join("verge.yaml");
atomic_write(&path, &bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_writable_file_path(path: &std::path::Path) -> anyhow::Result<()> {
    anyhow::ensure!(
        path.file_name().is_some(),
        "atomic_write needs a file path, got: {}",
        path.display()
    );
    Ok(())
}

Prevention

When it happens

Trigger: Calling `fs::atomic_write(path, contents)` with a path such as `/`, `C:\`, `some/dir/` (trailing slash), or any `Path` built without a final file component — `path.file_name()` returns `None` and the `ensure!` fires with the formatted path.

Common situations: Passing a directory instead of a file path; concatenating paths so a trailing separator survives (e.g. from user input or an env var); resolving a config path to its root/parent by mistake.

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 libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/c896710ce857e1e1. Report an issue: GitHub.