denoland/deno · error · FsError

Source and destination paths refer to the same file

Error message

Source and destination paths refer to the same file

What it means

copy_file() guards against copying a file onto itself: opening the destination with truncation before reading the source would silently empty the file, so Deno compares file identity with same_file::is_same_file (device + inode on Unix, file index + volume serial on Windows) and, if identical, returns io::ErrorKind::InvalidInput 'Source and destination paths refer to the same file' — matching cp behavior and libuv's message.

Source

Thrown at ext/fs/std_fs.rs:664

  res.map_err(Into::into)
}

fn copy_file(from: &Path, to: &Path) -> FsResult<()> {
  // Guard against copying a file onto itself. Otherwise the destination is
  // opened with truncation (or unlinked) before the source is read, which
  // silently empties the file. Match `cp` behavior and error instead.
  //
  // `same_file::is_same_file` compares the file identity (device + inode on
  // Unix, file index + volume serial via the open handle on Windows) using a
  // single stat per path, rather than fully canonicalizing both paths which
  // would `lstat`/`readlink` every component twice. It still catches
  // equivalent paths such as `./`, `..`, symlinks and hard links, and returns
  // `Err` (treated as "not the same file") in the common case where the
  // destination does not yet exist.
  if same_file::is_same_file(from, to).unwrap_or(false) {
    return Err(
      io::Error::new(
        io::ErrorKind::InvalidInput,
        "Source and destination paths refer to the same file",
      )
      .into(),
    );
  }

  #[cfg(target_os = "macos")]
  {
    use std::ffi::CString;
    use std::os::unix::fs::OpenOptionsExt;
    use std::os::unix::fs::PermissionsExt;

    use libc::clonefile;
    use libc::stat;
    use libc::unlink;

    let from_str = CString::new(from.as_os_str().as_encoded_bytes())

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Compare resolved paths first: skip when Deno.realPathSync(from) === Deno.realPathSync(to)
  2. Skip the call when the two argument strings are equal
  3. If you intended an overwrite workflow, delete the destination explicitly instead of copying onto itself
  4. Audit symlink/hardlink layouts when from and to look different but resolve identically

Example fix

// before
Deno.copyFileSync(fromPath, toPath);

// after
if (Deno.realPathSync(fromPath) !== Deno.realPathSync(toPath)) {
  Deno.copyFileSync(fromPath, toPath);
}
Defensive patterns

Strategy: validation

Validate before calling

function copyFileSyncIfDifferent(from: string, to: string): void {
  let fromReal: string, toReal: string;
  try {
    fromReal = Deno.realPathSync(from);
  } catch {
    throw new Error(`Source does not exist: ${from}`);
  }
  try {
    toReal = Deno.realPathSync(to);
  } catch {
    Deno.copyFileSync(from, to); // destination absent: safe
    return;
  }
  if (fromReal === toReal) return; // same inode: skip
  Deno.copyFileSync(from, to);
}

Type guard

function isSamePath(from: string, to: string): boolean {
  try {
    return Deno.realPathSync(from) === Deno.realPathSync(to);
  } catch {
    return false; // missing destination cannot be the same file
  }
}

Try / catch

try {
  Deno.copyFileSync(from, to);
} catch (e) {
  if (e instanceof Error && /refer to the same file/.test(e.message)) return;
  throw e;
}

Prevention

When it happens

Trigger: Deno.copyFileSync('a.txt', 'a.txt'); destination is a symlink or hard link resolving to the source inode (copyFileSync('a.txt', 'link-to-a.txt')); equivalent spellings like './a.txt' vs 'a.txt'; paths through symlinked directories that land on the same file.

Common situations: Backup scripts whose glob results include the destination; FROM/TO parameters mistakenly set to the same value; symlinked config or cache directories making two different strings the same file.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/e45e85f6007ab643. Report an issue: GitHub.