Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Improve File.Copy by using the block copy operation for ReFS on Windows #88695

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 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
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// 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
{
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)]
Expand All @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<char>(fileName), 0, ref Unsafe.NullRef<char>(), 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);
hamarb123 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/libraries/Common/tests/TestUtilities/TestUtilities.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
Link="Common\Interop\Windows\Kernel32\Interop.GetCurrentProcess.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs"
Link="Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.MAX_PATH.cs"
Link="Common\Interop\Windows\Kernel32\Interop.MAX_PATH.cs" />
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs"
Link="Common\Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs" />
<Compile Include="$(CommonPath)Interop\Windows\NtDll\Interop.RtlGetVersion.cs"
Expand Down
43 changes: 43 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/Copy.Windows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Xunit;

namespace System.IO.Tests
{
partial class File_Copy_Single
{
[Fact]
[PlatformSpecific(TestPlatforms.Windows)]
public unsafe void WindowsCheckSparseness()
{
string sourceFile = GetTestFilePath();
string destFile = GetTestFilePath();

File.WriteAllText(sourceFile, "abc");
File.WriteAllText(destFile, "def");

Assert.True((File.GetAttributes(sourceFile) & FileAttributes.SparseFile) == 0);
File.Copy(sourceFile, destFile, true);
Assert.True((File.GetAttributes(destFile) & FileAttributes.SparseFile) == 0);
Assert.Equal("abc", File.ReadAllText(sourceFile));

using (FileStream file = File.Open(sourceFile, FileMode.Open))
{
Assert.True(Interop.Kernel32.DeviceIoControl(file.SafeFileHandle, Interop.Kernel32.FSCTL_SET_SPARSE, null, 0, null, 0, out _, 0));
}
File.WriteAllText(destFile, "def");

Assert.True((File.GetAttributes(sourceFile) & FileAttributes.SparseFile) != 0);
File.Copy(sourceFile, destFile, true);
Assert.True((File.GetAttributes(destFile) & FileAttributes.SparseFile) != 0);
Assert.Equal("abc", File.ReadAllText(sourceFile));
}

// Todo: add a way to run all these on ReFS, and a test to check we actually cloned the reference, not just the data on ReFS.
}
}
Loading