diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DUPLICATE_EXTENTS_DATA.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DUPLICATE_EXTENTS_DATA.cs new file mode 100644 index 0000000000000..0423415b0b5b9 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DUPLICATE_EXTENTS_DATA.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://learn.microsoft.com/windows/win32/api/winioctl/ns-winioctl-duplicate_extents_data + internal struct DUPLICATE_EXTENTS_DATA + { + internal IntPtr FileHandle; + internal long SourceFileOffset; + internal long TargetFileOffset; + internal long ByteCount; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs index aae40b9bb7fdd..979758bbe7e90 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs @@ -15,6 +15,18 @@ internal static partial class Kernel32 // https://docs.microsoft.com/windows-hardware/drivers/ddi/ntddstor/ni-ntddstor-ioctl_storage_read_capacity internal const int IOCTL_STORAGE_READ_CAPACITY = 0x002D5140; + // https://learn.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_set_sparse + internal const int FSCTL_SET_SPARSE = 0x000900c4; + + // https://learn.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_get_integrity_information + internal const int FSCTL_GET_INTEGRITY_INFORMATION = 0x0009027C; + + // https://learn.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_set_integrity_information + internal const int FSCTL_SET_INTEGRITY_INFORMATION = 0x0009C280; + + // https://learn.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file + internal const int FSCTL_DUPLICATE_EXTENTS_TO_FILE = 0x00098344; + [LibraryImport(Libraries.Kernel32, EntryPoint = "DeviceIoControl", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool DeviceIoControl( diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_DISPOSITION_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_DISPOSITION_INFO.cs new file mode 100644 index 0000000000000..5f3e5c678d711 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_DISPOSITION_INFO.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // From FILE_INFO_BY_HANDLE_CLASS + // Use for SetFileInformationByHandle + internal const int FileDispositionInfo = 4; + + internal struct FILE_DISPOSITION_INFO + { + internal BOOLEAN DeleteFile; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_SET_SPARSE_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_SET_SPARSE_BUFFER.cs new file mode 100644 index 0000000000000..9b22e7076c9b2 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_SET_SPARSE_BUFFER.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://learn.microsoft.com/windows/win32/api/winioctl/ns-winioctl-file_set_sparse_buffer + internal struct FILE_SET_SPARSE_BUFFER + { + internal BOOLEAN SetSparse; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER.cs new file mode 100644 index 0000000000000..04d89fab7e396 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://learn.microsoft.com/windows/win32/api/winioctl/ns-winioctl-fsctl_get_integrity_information_buffer + internal struct FSCTL_GET_INTEGRITY_INFORMATION_BUFFER + { + internal ushort ChecksumAlgorithm; + internal ushort Reserved; + internal uint Flags; + internal uint ChecksumChunkSizeInBytes; + internal uint ClusterSizeInBytes; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER.cs new file mode 100644 index 0000000000000..c83b440f95660 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://learn.microsoft.com/windows/win32/api/winioctl/ns-winioctl-fsctl_set_integrity_information_buffer + internal struct FSCTL_SET_INTEGRITY_INFORMATION_BUFFER + { + internal ushort ChecksumAlgorithm; + internal ushort Reserved; + internal uint Flags; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileAttributes.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileAttributes.cs index c2350c35ec604..70ae7847537c9 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileAttributes.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileAttributes.cs @@ -10,6 +10,7 @@ internal static partial class FileAttributes internal const int FILE_ATTRIBUTE_NORMAL = 0x00000080; internal const int FILE_ATTRIBUTE_READONLY = 0x00000001; internal const int FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + internal const int FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200; internal const int FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GenericOperations.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GenericOperations.cs index f346fa89f8910..82c780384bf14 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GenericOperations.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GenericOperations.cs @@ -7,6 +7,7 @@ internal static partial class Kernel32 { internal static partial class GenericOperations { + internal const int DELETE = 0x00010000; internal const int GENERIC_READ = unchecked((int)0x80000000); internal const int GENERIC_WRITE = 0x40000000; } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetDiskFreeSpace.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetDiskFreeSpace.cs new file mode 100644 index 0000000000000..05595173dd0a9 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetDiskFreeSpace.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, EntryPoint = "GetDiskFreeSpaceW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool GetDiskFreeSpace(string drive, uint* sectorsPerCluster, uint* bytesPerSector, uint* numberOfFreeClusters, uint* totalNumberOfClusters); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs index cdc0fcc3e5319..aa978e149729f 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Win32.SafeHandles; internal static partial class Interop @@ -11,6 +14,7 @@ internal static partial class Interop internal static partial class Kernel32 { internal const uint FILE_NAME_NORMALIZED = 0x0; + internal const uint VOLUME_NAME_DOS = 0x0; // https://docs.microsoft.com/windows/desktop/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew (kernel32) [LibraryImport(Libraries.Kernel32, EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true)] @@ -19,5 +23,31 @@ internal static unsafe partial uint GetFinalPathNameByHandle( char* lpszFilePath, uint cchFilePath, uint dwFlags); + + internal static unsafe bool GetFinalPathNameByHandle(SafeFileHandle hFile, uint dwFlags, [MaybeNullWhen(false)] out string result) + { + // Default value + result = null; + + // Determine the required buffer size + uint bufferSize = GetFinalPathNameByHandle(hFile, null, 0, dwFlags); + if (bufferSize == 0) return false; + + // Allocate the buffer + ValueStringBuilder vsb = new(bufferSize <= Interop.Kernel32.MAX_PATH ? stackalloc char[Interop.Kernel32.MAX_PATH] : default); + vsb.EnsureCapacity((int)bufferSize); + + // Call the API + fixed (char* lpszFilePath = vsb.RawChars) + { + int length = (int)GetFinalPathNameByHandle(hFile, lpszFilePath, bufferSize, dwFlags); + if (length == 0) return false; + + // Return our string + vsb.Length = length; + result = vsb.ToString(); + return true; + } + } } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformation.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformation.cs index 4082bc4567776..e020179508083 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformation.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformation.cs @@ -21,5 +21,6 @@ internal static unsafe partial bool GetVolumeInformation( int fileSystemNameBufLen); internal const uint FILE_SUPPORTS_ENCRYPTION = 0x00020000; + internal const uint FILE_SUPPORTS_BLOCK_REFCOUNTING = 0x08000000; } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformationByHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformationByHandle.cs new file mode 100644 index 0000000000000..104b82bd91e45 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumeInformationByHandle.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, EntryPoint = "GetVolumeInformationByHandleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool GetVolumeInformationByHandle( + SafeFileHandle hFile, + char* volumeName, + uint volumeNameBufLen, + uint* volSerialNumber, + uint* maxFileNameLen, + uint* fileSystemFlags, + char* fileSystemName, + uint fileSystemNameBufLen); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumePathName.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumePathName.cs new file mode 100644 index 0000000000000..811cd2b3ab421 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetVolumePathName.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, EntryPoint = "GetVolumePathNameW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + private static unsafe partial bool GetVolumePathName(char* lpszFileName, char* lpszVolumePathName, int cchBufferLength); + + internal static unsafe string GetVolumePathName(string fileName) + { + // Ensure we have the prefix + fileName = PathInternal.EnsureExtendedPrefixIfNeeded(fileName); + + // Ensure our output buffer will be long enough (see https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumepathnamew#remarks) + int requiredBufferLength = (int)GetFullPathNameW(ref MemoryMarshal.GetReference(fileName), 0, ref Unsafe.NullRef(), 0); + if (requiredBufferLength == 0) throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastWin32Error(), fileName); + + // Allocate a value string builder + // note: MAX_PATH is not a hard limit, but would only be exceeded by a long path + ValueStringBuilder vsb = new(requiredBufferLength <= Interop.Kernel32.MAX_PATH ? stackalloc char[Interop.Kernel32.MAX_PATH] : default); + vsb.EnsureCapacity(requiredBufferLength); + + // Call the actual API + fixed (char* lpszFileName = fileName) + { + fixed (char* lpszVolumePathName = vsb.RawChars) + { + if (GetVolumePathName(lpszFileName, lpszVolumePathName, requiredBufferLength)) + { + vsb.Length = vsb.RawChars.IndexOf('\0'); + return vsb.ToString(); + } + } + } + + // Deal with error + int error = Marshal.GetLastWin32Error(); + Debug.Assert(error != Interop.Errors.ERROR_INSUFFICIENT_BUFFER); + throw Win32Marshal.GetExceptionForWin32Error(error, fileName); + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetEndOfFile.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetEndOfFile.cs new file mode 100644 index 0000000000000..ea27fa2c357d4 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetEndOfFile.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetEndOfFile(SafeFileHandle hFile); + } +} diff --git a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj index 2273e3bd088a2..794b39d6c119c 100644 --- a/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj +++ b/src/libraries/Common/tests/TestUtilities/TestUtilities.csproj @@ -60,6 +60,8 @@ Link="Common\Interop\Windows\Kernel32\Interop.GetCurrentProcess.cs" /> + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] - public sealed class File_Copy_Single : FileSystemTest + public sealed partial class File_Copy_Single : FileSystemTest { [Fact] public void EnsureThrowWhenCopyToNonSharedFile() @@ -418,5 +419,51 @@ public void EnsureThrowWhenCopyToNonSharedFile() using var stream = new FileStream(file1, FileMode.Open, FileAccess.Read, FileShare.None); Assert.Throws(() => File.Copy(file2, file1, overwrite: true)); } + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks)), + InlineData("", ""), + /*InlineData(":a", ""),*/ + InlineData("", ":a")/*, + InlineData(":a", ":a")*/] + //todo: is copying from an ADS meant to fail? + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsAlternateDataStreamSymlinkTest(string stream1, string stream2) + { + // This test checks copying all combinations of alternate data streams with all combinations of symlinks referencing them. + // This test exists to check we don't cause a BSOD when using ReFS block copy operation on alternative data streams (pending? rolled out fix from Windows team), and that it has the correct behaviour. + + string sourceFile = GetTestFilePath(); + string destFile = GetTestFilePath(); + + void Test(string src, string dst) + { + try + { + File.WriteAllText(sourceFile, "abc"); + File.WriteAllText(destFile, "def"); + File.WriteAllText(sourceFile + stream1, "ghi"); + File.WriteAllText(destFile + stream2, "jkl"); + + File.Copy(src, dst, true); + + if (stream1 != "") Assert.Equal("abc", File.ReadAllText(sourceFile)); + if (stream2 != "") Assert.Equal("def", File.ReadAllText(destFile)); + Assert.Equal("ghi", File.ReadAllText(sourceFile + stream1)); + Assert.Equal("ghi", File.ReadAllText(destFile + stream2)); + } + catch (Exception ex) + { + throw new Exception($"Failed with src={src}, dst={dst}.", ex); + } + } + + File.CreateSymbolicLink(sourceFile + ".link", sourceFile + stream1); + File.CreateSymbolicLink(destFile + ".link", destFile + stream2); + + Test(sourceFile + stream1, destFile + stream2); + Test(sourceFile + stream1, destFile + ".link"); + Test(sourceFile + ".link", destFile + stream2); + Test(sourceFile + ".link", destFile + ".link"); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 7547ddc53b2bb..d549aefd20b74 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -91,6 +91,7 @@ + @@ -103,11 +104,13 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 14b8a81473267..25ce1b31e8993 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1705,6 +1705,18 @@ Common\Interop\Windows\Kernel32\Interop.DeviceIoControl.cs + + Common\Interop\Windows\Kernel32\Interop.FILE_SET_SPARSE_BUFFER.cs + + + Common\Interop\Windows\Kernel32\Interop.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER.cs + + + Common\Interop\Windows\Kernel32\Interop.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER.cs + + + Common\Interop\Windows\Kernel32\Interop.DUPLICATE_EXTENTS_DATA.cs + Common\Interop\Windows\Kernel32\Interop.ExpandEnvironmentStrings.cs @@ -1714,6 +1726,9 @@ Common\Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs + + Common\Interop\Windows\Kernel32\Interop.FILE_DISPOSITION_INFO.cs + Common\Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs @@ -1780,6 +1795,9 @@ Common\Interop\Windows\Kernel32\Interop.GetCurrentProcessId.cs + + Common\Interop\Windows\Kernel32\Interop.GetDiskFreeSpace.cs + Interop\Windows\Kernel32\Interop.GetCurrentThreadId.cs @@ -1840,6 +1858,12 @@ Common\Interop\Windows\Kernel32\Interop.GetVolumeInformation.cs + + Common\Interop\Windows\Kernel32\Interop.GetVolumeInformationByHandle.cs + + + Common\Interop\Windows\Kernel32\Interop.GetVolumePathName.cs + Common\Interop\Windows\Kernel32\Interop.GlobalMemoryStatusEx.cs @@ -1942,6 +1966,9 @@ Common\Interop\Windows\Kernel32\Interop.SetCurrentDirectory.cs + + Common\Interop\Windows\Kernel32\Interop.SetEndOfFile.cs + Common\Interop\Windows\Kernel32\Interop.SetFileAttributes.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs index fc063209e36fc..404136198110e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs @@ -56,8 +56,320 @@ private static unsafe void ThrowExceptionEncryptDecryptFail(string fullPath) throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath); } + private static unsafe bool TryCloneFile(string sourceFullPath, string destFullPath, bool overwrite) + { + // Open the source file. + // We use FILE_FLAGS_NO_BUFFERING since we're not using unaligned writes during cloning and can skip buffering overhead. + const int FILE_FLAG_NO_BUFFERING = 0x20000000; + using (SafeFileHandle sourceHandle = Interop.Kernel32.CreateFile( + sourceFullPath, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.Read, + FileMode.Open, + FILE_FLAG_NO_BUFFERING)) + { + // Return false if we failed to open the source file. + if (sourceHandle.IsInvalid) + { + return false; + } + + // Read the source's volume's info. + uint sourceSerialNumber; + uint sourceVolumeFlags; + if (!Interop.Kernel32.GetVolumeInformationByHandle(sourceHandle, null, 0, &sourceSerialNumber, null, &sourceVolumeFlags, null, 0)) + { + throw new Exception("C"); + //return false; + } + + // Check if it supports the block copy operation. + if ((sourceVolumeFlags & Interop.Kernel32.FILE_SUPPORTS_BLOCK_REFCOUNTING) == 0) + { + throw new Exception("D"); + //return false; + } + + // Helper variables. + SafeFileHandle? destinationHandle = null; + bool madeNew = false; + + try + { + // Open the destination, keeping track of whether we made a file. + Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA destFileInformation = default; + if (overwrite) + { + // Try to open the existing file. + destinationHandle = Interop.Kernel32.CreateFile( + destFullPath, + Interop.Kernel32.GenericOperations.GENERIC_READ | Interop.Kernel32.GenericOperations.GENERIC_WRITE, + FileShare.None, + FileMode.Open, + 0); + if (destinationHandle.IsInvalid) + { + // Try opening as a new file. + destinationHandle = null; + } + } + + // If we still haven't opened the file. + if (destinationHandle == null) + { + // Try to create the destination file. + destinationHandle = Interop.Kernel32.CreateFile( + destFullPath, + Interop.Kernel32.GenericOperations.GENERIC_READ | Interop.Kernel32.GenericOperations.GENERIC_WRITE | Interop.Kernel32.GenericOperations.DELETE, + FileShare.None, + FileMode.CreateNew, + 0); + if (destinationHandle.IsInvalid) + { + // Failure to open. + return false; + } + madeNew = true; + } + + // Read the destination file's volume's serial number. + uint destSerialNumber; + uint destVolumeFlags; + if (!Interop.Kernel32.GetVolumeInformationByHandle(sourceHandle, null, 0, &destSerialNumber, null, &destVolumeFlags, null, 0)) + { + throw new Exception("G"); + //return false; + } + + // Check the destination file is on the same volume. + // Note: this doesn't guarantee they're on the same volume for sure, so we have to take that into consideration later: https://devblogs.microsoft.com/oldnewthing/20170707-00/?p=96555. + if (sourceSerialNumber != destSerialNumber) + { + throw new Exception("H"); + //return false; + } + + // Quick sanity check to see if the destination supports the block copy operation, since it's basically free to check anyway. + if ((destVolumeFlags & Interop.Kernel32.FILE_SUPPORTS_BLOCK_REFCOUNTING) == 0) + { + throw new Exception("I"); + //return false; + } + + // Get the source file path. We need to do this, because we may have opened a symlink - this returns the file we actually have open. + if (!Interop.Kernel32.GetFinalPathNameByHandle(sourceHandle, Interop.Kernel32.FILE_NAME_NORMALIZED | Interop.Kernel32.VOLUME_NAME_DOS, out string? sourceName)) + { + // This may fail as a result of it not being on a drive with a DOS path, we shouldn't throw here + throw new Exception("I2"); + //return false; + } + + // Check we haven't opened an alternate data stream - this may cause a BSOD on Windows versions lower than TBD (todo) if we don't exit before block copy. + if (Path.GetFileName(sourceName.AsSpan()).Contains(':')) + { + return false; + } + + // Also check the destination file + if (!Interop.Kernel32.GetFinalPathNameByHandle(destinationHandle, Interop.Kernel32.FILE_NAME_NORMALIZED | Interop.Kernel32.VOLUME_NAME_DOS, out string? destName)) + { + throw new Exception("I3"); + //return false; + } + if (Path.GetFileName(destName.AsSpan()).Contains(':')) + { + return false; + } + + // Get the source volume path. Note: we need the real path here for symlinks also, hence sourceName. + // Todo: do we care about not propogating an error from this? + string volumePath = Interop.Kernel32.GetVolumePathName(sourceName); + + // Read the source volume's cluster size. + uint sectorsPerCluster, bytesPerSector; + if (!Interop.Kernel32.GetDiskFreeSpace(volumePath!, §orsPerCluster, &bytesPerSector, null, null)) + { + throw new Exception("K"); + //return false; + } + long clusterSize = sectorsPerCluster * (long)bytesPerSector; + + // Set file length to 0. + if (!madeNew) + { + if (!Interop.Kernel32.SetEndOfFile(destinationHandle)) + { + throw new Exception("O"); + //return false; + } + } + + // Ensure the destination file is not readonly. + // Todo: are there other flags we need to exclude here? + FileAttributes newAttributes = (FileAttributes)destFileInformation.dwFileAttributes & ~(FileAttributes.ReadOnly | FileAttributes.Normal); + if (newAttributes == 0) newAttributes = FileAttributes.Normal; + Interop.Kernel32.FILE_BASIC_INFO basicInfo = new() + { + FileAttributes = (uint)newAttributes, + }; + if (!Interop.Kernel32.SetFileInformationByHandle( + destinationHandle, + Interop.Kernel32.FileBasicInfo, + &basicInfo, + (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(destinationHandle.Path); + } + + // Make the destination sparse. + if (!Interop.Kernel32.DeviceIoControl(destinationHandle, Interop.Kernel32.FSCTL_SET_SPARSE, null, 0, null, 0, out _, 0)) + { + throw new Exception("Q3"); + } + + // Clone file integrity settings from source to destination. Must be done while file is zero size. + Interop.Kernel32.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER getIntegrityInfo; + if (!Interop.Kernel32.DeviceIoControl( + sourceHandle, + Interop.Kernel32.FSCTL_GET_INTEGRITY_INFORMATION, + null, + 0, + &getIntegrityInfo, + (uint)sizeof(Interop.Kernel32.FSCTL_GET_INTEGRITY_INFORMATION_BUFFER), + out _, + IntPtr.Zero)) + { + throw new Exception("Q"); + //return false; + } + if (!madeNew || getIntegrityInfo.ChecksumAlgorithm != 0 || getIntegrityInfo.Flags != 0) + { + Interop.Kernel32.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setIntegrityInfo; + setIntegrityInfo.ChecksumAlgorithm = getIntegrityInfo.ChecksumAlgorithm; + setIntegrityInfo.Reserved = getIntegrityInfo.Reserved; + setIntegrityInfo.Flags = getIntegrityInfo.Flags; + if (!Interop.Kernel32.DeviceIoControl( + destinationHandle, + Interop.Kernel32.FSCTL_SET_INTEGRITY_INFORMATION, + &setIntegrityInfo, + (uint)sizeof(Interop.Kernel32.FSCTL_SET_INTEGRITY_INFORMATION_BUFFER), + null, + 0, + out _, + IntPtr.Zero)) + { + throw new Exception("R"); + //return false; + } + } + + // Read the file attributes. + Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA sourceFileInformation = default; + int error = FillAttributeInfo(sourceHandle, ref sourceFileInformation); + Debug.Assert(error == 0); //todo: is there a good reason for this to fail? if so we should handle it properly, not by a Debug.Assert + + // Get file size. + long sourceSize = (long)(((ulong)sourceFileInformation.nFileSizeHigh << 32) | sourceFileInformation.nFileSizeLow); + + // Set length of destination to same as source. + Interop.Kernel32.FILE_END_OF_FILE_INFO eofInfo; + eofInfo.EndOfFile = sourceSize; + if (!Interop.Kernel32.SetFileInformationByHandle(destinationHandle, Interop.Kernel32.FileEndOfFileInfo, &eofInfo, (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) + { + throw new Exception("S"); + //return false; + } + + // Copy all of the blocks. + Interop.Kernel32.DUPLICATE_EXTENTS_DATA duplicateExtentsData; + duplicateExtentsData.FileHandle = sourceHandle.DangerousGetHandle(); + // ReFS requires that cloned regions reside on a disk cluster boundary. + long clusterSizeMask = clusterSize - 1; + long fileSizeRoundedUpToClusterBoundary = (sourceSize + clusterSizeMask) & ~clusterSizeMask; + long sourceOffset = 0; + while (sourceOffset < sourceSize) + { + duplicateExtentsData.SourceFileOffset = sourceOffset; + duplicateExtentsData.TargetFileOffset = sourceOffset; + const long MaxChunkSize = 1L << 31; // Each cloned region must be < 4GiB in length. Use a smaller default (2GiB). + long thisChunkSize = Math.Min(fileSizeRoundedUpToClusterBoundary - sourceOffset, MaxChunkSize); + duplicateExtentsData.ByteCount = thisChunkSize; + + if (!Interop.Kernel32.DeviceIoControl( + destinationHandle, + Interop.Kernel32.FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &duplicateExtentsData, + (uint)sizeof(Interop.Kernel32.DUPLICATE_EXTENTS_DATA), + null, + 0, + out _, + IntPtr.Zero)) + { + throw new Exception("U1"); + //return false; + } + + sourceOffset += thisChunkSize; + } + + // Match source sparseness. + if ((sourceFileInformation.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_SPARSE_FILE) == 0) + { + Interop.Kernel32.FILE_SET_SPARSE_BUFFER sparseBuffer; + sparseBuffer.SetSparse = Interop.BOOLEAN.FALSE; + Interop.Kernel32.DeviceIoControl(destinationHandle, Interop.Kernel32.FSCTL_SET_SPARSE, &sparseBuffer, (uint)sizeof(Interop.Kernel32.FILE_SET_SPARSE_BUFFER), null, 0, out _, 0); + } + + // Update the attributes and times. + basicInfo = new Interop.Kernel32.FILE_BASIC_INFO + { + CreationTime = sourceFileInformation.ftCreationTime.ToTicks(), + LastAccessTime = sourceFileInformation.ftLastAccessTime.ToTicks(), + LastWriteTime = sourceFileInformation.ftLastWriteTime.ToTicks(), + FileAttributes = (uint)sourceFileInformation.dwFileAttributes, + }; + if (!Interop.Kernel32.SetFileInformationByHandle( + destinationHandle, + Interop.Kernel32.FileBasicInfo, + &basicInfo, + (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(destinationHandle.Path); + } + + // We have now succeeded, and don't want to delete the destination. + madeNew = false; + return true; + } + finally + { + if (madeNew) + { + // Mark the file for deletion. + Interop.Kernel32.FILE_DISPOSITION_INFO dispositionInfo; + dispositionInfo.DeleteFile = Interop.BOOLEAN.TRUE; + if (!Interop.Kernel32.SetFileInformationByHandle( + destinationHandle!, + Interop.Kernel32.FileDispositionInfo, + &dispositionInfo, + (uint)sizeof(Interop.Kernel32.FILE_DISPOSITION_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(destinationHandle!.Path); + } + } + + destinationHandle?.Dispose(); + } + } + } + public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite) { + if (TryCloneFile(sourceFullPath, destFullPath, overwrite)) + { + return; + } + int errorCode = Interop.Kernel32.CopyFile(sourceFullPath, destFullPath, !overwrite); if (errorCode != Interop.Errors.ERROR_SUCCESS)