Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
144 changes: 135 additions & 9 deletions src/Polyfill/FilePolyfill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polyfills;
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down Expand Up @@ -132,20 +133,12 @@ public static Task AppendAllBytesAsync(string path, ReadOnlyMemory<byte> bytes,
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.file.move?view=net-11.0#system-io-file-move(system-string-system-string-system-boolean)
public static void Move(string sourceFileName, string destFileName, bool overwrite)
{
sourceFileName = Path.GetFullPath(sourceFileName);
destFileName = Path.GetFullPath(destFileName);
if (sourceFileName == destFileName)
{
return;
}

#if NETCOREAPP3_0_OR_GREATER
File.Move(sourceFileName, destFileName, overwrite: overwrite);
#else
if (overwrite)
{
File.Copy(sourceFileName, destFileName, overwrite: true);
File.Delete(sourceFileName);
FileMoveHelper.MoveAndOverwrite(sourceFileName, destFileName);
}
else
{
Expand Down Expand Up @@ -379,6 +372,139 @@ public static async IAsyncEnumerable<string> ReadLinesAsync(string path, Encodin
#endif

}

#if !NETCOREAPP3_0_OR_GREATER
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
#if PolyUseEmbeddedAttribute
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
#endif
static class FileMoveHelper
{
const uint MOVEFILE_REPLACE_EXISTING = 1;
const uint MOVEFILE_COPY_ALLOWED = 2;

const int ERROR_FILE_NOT_FOUND = 2;
const int ERROR_PATH_NOT_FOUND = 3;
const int ERROR_ACCESS_DENIED = 5;

const int EPERM = 1;
const int ENOENT = 2;
const int EACCES = 13;
const int EXDEV = 18;

// A rename that replaces the destination, as File.Move(string, string, bool) does on netcoreapp3.0 and up.
internal static void MoveAndOverwrite(string sourceFileName, string destFileName)
{
ValidatePath(sourceFileName, nameof(sourceFileName));
ValidatePath(destFileName, nameof(destFileName));

var source = Path.GetFullPath(sourceFileName);
var dest = Path.GetFullPath(destFileName);

if (!File.Exists(source))
{
throw new FileNotFoundException($"Could not find file '{source}'.", source);
}

#if FeatureRuntimeInformation
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
#else
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
#endif
{
if (!MoveFileExW(ToExtendedPath(source), ToExtendedPath(dest), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))
{
throw CreateWindowsException(Marshal.GetLastWin32Error(), source);
}

return;
}

if (rename(source, dest) == 0)
{
return;
}

var error = Marshal.GetLastWin32Error();
if (error == EXDEV)
{
// rename does not cross file systems
File.Copy(source, dest, overwrite: true);
File.Delete(source);
return;
}

throw CreateUnixException(error, source, dest);
}

static void ValidatePath(string path, string paramName)
{
if (path is null)
{
throw new ArgumentNullException(paramName);
}

if (path.Length == 0)
{
throw new ArgumentException("The value cannot be an empty string.", paramName);
}
}

// MoveFileEx is limited to MAX_PATH unless the path has the extended prefix
static string ToExtendedPath(string fullPath)
{
if (fullPath.Length < 260 ||
fullPath.StartsWith(@"\\?\", StringComparison.Ordinal))
{
return fullPath;
}

if (fullPath.StartsWith(@"\\", StringComparison.Ordinal))
{
return @"\\?\UNC\" + fullPath.Substring(2);
}

return @"\\?\" + fullPath;
}

static Exception CreateWindowsException(int error, string source)
{
switch (error)
{
case ERROR_FILE_NOT_FOUND:
return new FileNotFoundException($"Could not find file '{source}'.", source);
case ERROR_PATH_NOT_FOUND:
return new DirectoryNotFoundException("Could not find a part of the path.");
case ERROR_ACCESS_DENIED:
return new UnauthorizedAccessException("Access to the path is denied.");
default:
return new IOException(new Win32Exception(error).Message, unchecked((int) 0x80070000) | error);
}
}

static Exception CreateUnixException(int error, string source, string dest)
{
switch (error)
{
case ENOENT:
// the source exists, so it is the destination directory that is missing
return new DirectoryNotFoundException("Could not find a part of the path.");
case EPERM:
case EACCES:
return new UnauthorizedAccessException("Access to the path is denied.");
default:
return new IOException($"Unable to move file '{source}' to '{dest}'.", error);
}
}

[DllImport("kernel32.dll", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool MoveFileExW(string lpExistingFileName, string lpNewFileName, uint dwFlags);

[DllImport("libc", SetLastError = true)]
static extern int rename(string oldpath, string newpath);
}
#endif
#endif

#if !NET11_0_OR_GREATER
Expand Down
1 change: 1 addition & 0 deletions src/Split/net10.0/FilePolyfill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Polyfills;
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down
1 change: 1 addition & 0 deletions src/Split/net11.0/FilePolyfill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Polyfills;
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down
117 changes: 109 additions & 8 deletions src/Split/net461/FilePolyfill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Polyfills;
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down Expand Up @@ -102,16 +103,9 @@ public static Task AppendAllBytesAsync(string path, ReadOnlyMemory<byte> bytes,
/// </summary>
public static void Move(string sourceFileName, string destFileName, bool overwrite)
{
sourceFileName = Path.GetFullPath(sourceFileName);
destFileName = Path.GetFullPath(destFileName);
if (sourceFileName == destFileName)
{
return;
}
if (overwrite)
{
File.Copy(sourceFileName, destFileName, overwrite: true);
File.Delete(sourceFileName);
FileMoveHelper.MoveAndOverwrite(sourceFileName, destFileName);
}
else
{
Expand Down Expand Up @@ -286,6 +280,113 @@ public static async IAsyncEnumerable<string> ReadLinesAsync(string path, Encodin
}
#endif
}
[ExcludeFromCodeCoverage]
[DebuggerNonUserCode]
#if PolyUseEmbeddedAttribute
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
#endif
static class FileMoveHelper
{
const uint MOVEFILE_REPLACE_EXISTING = 1;
const uint MOVEFILE_COPY_ALLOWED = 2;
const int ERROR_FILE_NOT_FOUND = 2;
const int ERROR_PATH_NOT_FOUND = 3;
const int ERROR_ACCESS_DENIED = 5;
const int EPERM = 1;
const int ENOENT = 2;
const int EACCES = 13;
const int EXDEV = 18;
internal static void MoveAndOverwrite(string sourceFileName, string destFileName)
{
ValidatePath(sourceFileName, nameof(sourceFileName));
ValidatePath(destFileName, nameof(destFileName));
var source = Path.GetFullPath(sourceFileName);
var dest = Path.GetFullPath(destFileName);
if (!File.Exists(source))
{
throw new FileNotFoundException($"Could not find file '{source}'.", source);
}
#if FeatureRuntimeInformation
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
#else
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
#endif
{
if (!MoveFileExW(ToExtendedPath(source), ToExtendedPath(dest), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))
{
throw CreateWindowsException(Marshal.GetLastWin32Error(), source);
}
return;
}
if (rename(source, dest) == 0)
{
return;
}
var error = Marshal.GetLastWin32Error();
if (error == EXDEV)
{
File.Copy(source, dest, overwrite: true);
File.Delete(source);
return;
}
throw CreateUnixException(error, source, dest);
}
static void ValidatePath(string path, string paramName)
{
if (path is null)
{
throw new ArgumentNullException(paramName);
}
if (path.Length == 0)
{
throw new ArgumentException("The value cannot be an empty string.", paramName);
}
}
static string ToExtendedPath(string fullPath)
{
if (fullPath.Length < 260 ||
fullPath.StartsWith(@"\\?\", StringComparison.Ordinal))
{
return fullPath;
}
if (fullPath.StartsWith(@"\\", StringComparison.Ordinal))
{
return @"\\?\UNC\" + fullPath.Substring(2);
}
return @"\\?\" + fullPath;
}
static Exception CreateWindowsException(int error, string source)
{
switch (error)
{
case ERROR_FILE_NOT_FOUND:
return new FileNotFoundException($"Could not find file '{source}'.", source);
case ERROR_PATH_NOT_FOUND:
return new DirectoryNotFoundException("Could not find a part of the path.");
case ERROR_ACCESS_DENIED:
return new UnauthorizedAccessException("Access to the path is denied.");
default:
return new IOException(new Win32Exception(error).Message, unchecked((int) 0x80070000) | error);
}
}
static Exception CreateUnixException(int error, string source, string dest)
{
switch (error)
{
case ENOENT:
return new DirectoryNotFoundException("Could not find a part of the path.");
case EPERM:
case EACCES:
return new UnauthorizedAccessException("Access to the path is denied.");
default:
return new IOException($"Unable to move file '{source}' to '{dest}'.", error);
}
}
[DllImport("kernel32.dll", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool MoveFileExW(string lpExistingFileName, string lpNewFileName, uint dwFlags);
[DllImport("libc", SetLastError = true)]
static extern int rename(string oldpath, string newpath);
}
extension(File)
{
/// <summary>
Expand Down
Loading
Loading