Skip to content

Use a real rename for File.Move(overwrite) polyfill - #664

Merged
SimonCropp merged 1 commit into
mainfrom
fix/file-move-overwrite
Oct 11, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
fix/file-move-overwrite

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Bug

Below netcoreapp3.0 the File.Move(sourceFileName, destFileName, overwrite: true) polyfill was File.Copy followed by File.Delete:

  • A read-only source was copied and then failed to delete with UnauthorizedAccessException, leaving both files behind.
  • A case-only rename (name.txt to NAME.txt) failed with a sharing violation, because the copy opened the same file twice.
  • It was not atomic: a failure part way through could leave the destination replaced or partly written with the source still present.
  • When source and destination were the same path it returned without doing anything, even if the file did not exist. The BCL throws FileNotFoundException.

Fix

Do a real rename that replaces the destination, as the BCL does:

  • Windows: MoveFileEx with MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED. Paths of 260 characters or more get the \\?\ prefix.
  • Unix: rename(2), falling back to copy and delete only for EXDEV (a move across file systems).

Exceptions follow the BCL, which I checked against net10 on Windows: null or empty arguments, FileNotFoundException for a missing source (checked first, so it also applies when both paths are the same), DirectoryNotFoundException for a missing destination directory, UnauthorizedAccessException for access denied (for example a read-only destination), IOException otherwise.

The Unix path was not executed: I only had Windows available, and the repo's tests run on Windows CI.

Tests

New Move_Overwrite_* tests in FilePolyfillTests, each in its own temp directory: read-only source, case-only rename, same missing path, same existing path, missing source keeps the destination, missing destination directory keeps the source, and read-only destination keeps both (Windows only).

ReadOnlySource, CaseOnlyRename and SameMissingPath fail on net48 without the fix. All pass on net48 (Tests and NoRefsTests) and on net10.0 (BCL), and the whole FilePolyfillTests class passes on net48.

@SimonCropp SimonCropp added this to the 11.4.4 milestone Oct 11, 2026
@SimonCropp
SimonCropp merged commit 8576477 into main Oct 11, 2026
5 checks passed
@SimonCropp
SimonCropp deleted the fix/file-move-overwrite branch October 11, 2026 22:10
This was referenced Oct 12, 2026
This was referenced Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant