Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
File.Unix: Replace: increase Windows compatibility
- When source and destination are not a file throw UnauthorizedAccessException.
- When source and destination are the same file throw IOException.
  • Loading branch information
tmds committed Mar 25, 2021
commit 80e9b0fb20c717087adf33bc805cfa92a6f0eeae
6 changes: 6 additions & 0 deletions src/libraries/System.IO.FileSystem/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@
<data name="IO_CannotCreateDirectory" xml:space="preserve">
<value>The specified directory '{0}' cannot be created.</value>
</data>
<data name="IO_CannotReplaceSameFile" xml:space="preserve">
<value>The source and destination are the same file.</value>
</data>
<data name="IO_EOF_ReadBeyondEOF" xml:space="preserve">
<value>Unable to read beyond the end of the stream.</value>
</data>
Expand All @@ -218,6 +221,9 @@
<data name="IO_FileTooLongOrHandleNotSync" xml:space="preserve">
<value>IO operation will not work. Most likely the file will become too long or the handle was not opened to support synchronous IO operations.</value>
</data>
<data name="IO_NotAFile" xml:space="preserve">
<value>The specified path '{0}' is not a file.</value>
</data>
<data name="IO_PathNotFound_NoPathName" xml:space="preserve">
<value>Could not find a part of the path.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,36 @@ private static void LinkOrCopyFile (string sourceFullPath, string destFullPath)

public static void ReplaceFile(string sourceFullPath, string destFullPath, string? destBackupFullPath, bool ignoreMetadataErrors)
{
// Unix rename works in more cases, we limit to what is allowed by Windows File.Replace.
// These checks are not atomic, the file could change after a check was performed and before it is renamed.
Interop.Sys.FileStatus sourceStat;
if (Interop.Sys.LStat(sourceFullPath, out sourceStat) != 0)
{
Interop.ErrorInfo errno = Interop.Sys.GetLastErrorInfo();
throw Interop.GetExceptionForIoErrno(errno, sourceFullPath);
}
// Check source is not a directory.
if ((sourceStat.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
{
throw new UnauthorizedAccessException(SR.Format(SR.IO_NotAFile, sourceFullPath));
}

Interop.Sys.FileStatus destStat;
if (Interop.Sys.LStat(destFullPath, out destStat) == 0)
{
// Check destination is not a directory.
if ((destStat.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)
{
throw new UnauthorizedAccessException(SR.Format(SR.IO_NotAFile, destFullPath));
}
// Check source and destination are not the same.
if (sourceStat.Dev == destStat.Dev &&
sourceStat.Ino == destStat.Ino)
{
throw new IOException(SR.IO_CannotReplaceSameFile);
}
}

if (destBackupFullPath != null)
{
// We're backing up the destination file to the backup file, so we need to first delete the backup
Expand Down
31 changes: 31 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/Replace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,37 @@ public void InvalidFileNames()
Assert.Throws<ArgumentException>(() => Replace(testFile, testFile2, "*\0*"));
}

[Fact]
public void SourceCannotBeADirectory()
{
string testFile = GetTestFilePath();
File.Create(testFile).Dispose();
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);

Assert.Throws<UnauthorizedAccessException>(() => File.Replace(testDir, testFile, null));
}

[Fact]
public void DestinationCannotBeADirectory()
{
string testFile = GetTestFilePath();
File.Create(testFile).Dispose();
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);

Assert.Throws<UnauthorizedAccessException>(() => File.Replace(testFile, testDir, null));
}

[Fact]
public void SourceAndDestinationCannotBeTheSame()
{
string testFile = GetTestFilePath();
File.Create(testFile).Dispose();

Assert.Throws<IOException>(() => File.Replace(testFile, testFile, null));
}

#endregion
}

Expand Down