Skip to content

Commit 3a0147c

Browse files
committed
Fix case-sensitive rename.
1 parent f53220a commit 3a0147c

File tree

3 files changed

+34
-24
lines changed

3 files changed

+34
-24
lines changed

src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ private static void CreateParentsAndDirectory(string fullPath)
387387
}
388388
}
389389

390-
internal static void MoveDirectory(string sourceFullPath, string destFullPath)
390+
private static void MoveDirectoryCore(string sourceFullPath, string destFullPath)
391391
{
392392
ReadOnlySpan<char> destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan());
393393
ReadOnlySpan<char> srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan());
@@ -399,7 +399,7 @@ internal static void MoveDirectory(string sourceFullPath, string destFullPath)
399399
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
400400
}
401401

402-
// The destination must not exist.
402+
// The destination must not exist (unless it is a case-sensitive rename).
403403
// On Unix 'rename' will overwrite the destination file if it already exists, we need to manually check.
404404
if (Interop.Sys.LStat(destNoDirectorySeparator, out Interop.Sys.FileStatus destFileStatus) >= 0)
405405
{
@@ -410,30 +410,32 @@ internal static void MoveDirectory(string sourceFullPath, string destFullPath)
410410
{
411411
throw new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
412412
}
413-
// Source and destination must not be the same file.
413+
// Source and destination must not be the same file unless it is a case-sensitive rename.
414414
else if (sourceFileStatus.Dev == destFileStatus.Dev &&
415415
sourceFileStatus.Ino == destFileStatus.Ino)
416416
{
417-
throw new IOException(SR.IO_SourceDestMustBeDifferent);
417+
// Assume the file system is case-insensitive, and allow a Rename when the FileName casing changes.
418+
if (!srcNoDirectorySeparator.Equals(destNoDirectorySeparator, StringComparison.OrdinalIgnoreCase) ||
419+
Path.GetFileName(srcNoDirectorySeparator).SequenceEqual(Path.GetFileName(destNoDirectorySeparator)))
420+
{
421+
throw new IOException(SR.IO_SourceDestMustBeDifferent);
422+
}
423+
// Fall through to Rename.
418424
}
419425
// When the path ends with a directory separator, it must be a directory.
420426
else if ((sourceFileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFDIR
421427
&& Path.EndsInDirectorySeparator(sourceFullPath))
422428
{
423429
throw new IOException(SR.Format(SR.IO_PathNotFound_Path, sourceFullPath));
424430
}
425-
426-
throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath));
431+
else
432+
{
433+
throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, destFullPath));
434+
}
427435
}
428436

429437
if (Interop.Sys.Rename(sourceFullPath, destNoDirectorySeparator) < 0)
430438
{
431-
// Source and destination must not be the same file.
432-
if (srcNoDirectorySeparator.Equals(destNoDirectorySeparator, PathInternal.StringComparison))
433-
{
434-
throw new IOException(SR.IO_SourceDestMustBeDifferent);
435-
}
436-
437439
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
438440
switch (errorInfo.Error)
439441
{

src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public static DateTimeOffset GetLastWriteTime(string fullPath)
154154
return data.ftLastWriteTime.ToDateTimeOffset();
155155
}
156156

157-
internal static void MoveDirectory(string sourceFullPath, string destFullPath)
157+
private static void MoveDirectoryCore(string sourceFullPath, string destFullPath)
158158
{
159159
// Source and destination must have the same root.
160160
ReadOnlySpan<char> sourceRoot = Path.GetPathRoot(sourceFullPath);
@@ -166,17 +166,6 @@ internal static void MoveDirectory(string sourceFullPath, string destFullPath)
166166

167167
if (!Interop.Kernel32.MoveFile(sourceFullPath, destFullPath, overwrite: false))
168168
{
169-
// Source and destination must not be the same file.
170-
// We don't check upfront, but handle this as a MoveFile failure.
171-
// MoveFile will fail because it requires the source to exist and the destination to not exist.
172-
// That is not possible if they are the same file.
173-
ReadOnlySpan<char> srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan());
174-
ReadOnlySpan<char> destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan());
175-
if (srcNoDirectorySeparator.Equals(destNoDirectorySeparator, PathInternal.StringComparison))
176-
{
177-
throw new IOException(SR.IO_SourceDestMustBeDifferent);
178-
}
179-
180169
int errorCode = Marshal.GetLastWin32Error();
181170

182171
if (errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND)

src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,24 @@ internal static void VerifyValidPath(string path, string argName)
1313
throw new ArgumentException(SR.Argument_InvalidPathChars, argName);
1414
}
1515
}
16+
17+
internal static void MoveDirectory(string sourceFullPath, string destFullPath)
18+
{
19+
ReadOnlySpan<char> srcNoDirectorySeparator = Path.TrimEndingDirectorySeparator(sourceFullPath.AsSpan());
20+
ReadOnlySpan<char> destNoDirectorySeparator = Path.TrimEndingDirectorySeparator(destFullPath.AsSpan());
21+
22+
// Don't allow the same path, except for changing the casing of the filename.
23+
if (srcNoDirectorySeparator.Equals(destNoDirectorySeparator, PathInternal.StringComparison))
24+
{
25+
ReadOnlySpan<char> srcFileName = Path.GetFileName(srcNoDirectorySeparator);
26+
ReadOnlySpan<char> destFileName = Path.GetFileName(destNoDirectorySeparator);
27+
if (srcFileName.SequenceEqual(destFileName))
28+
{
29+
throw new IOException(SR.IO_SourceDestMustBeDifferent);
30+
}
31+
}
32+
33+
MoveDirectoryCore(sourceFullPath, destFullPath);
34+
}
1635
}
1736
}

0 commit comments

Comments
 (0)