On the command line it is simply as follows
fsutil file queryvaliddata
For example, a file created with Fsutil file createnew C:\largefile 53687091200
will have a 50gb "EOF", but a zero byte "VDL" - it will have a 50gb "NVDL"
Tracing fsutil.exe with procmon
reveals a call to FSCTL_QUERY_FILE_REGIONS
in the Win32Api
This is the code you need in C# - the documentation was slim and theres 0 existing help for doing this in .Net, but I got there in the end.
Note that just as with fsutil file queryvaliddata /D
, there will only ever be one NVDL area.
public class NTFSValidData
{
// Define the FSCTL_QUERY_FILE_REGIONS control code
private const uint FSCTL_QUERY_FILE_REGIONS = 0x00090284;
private const uint FILE_REGION_USAGE_VALID_NONCACHED_DATA = 0x00000002;
private const uint FILE_REGION_USAGE_VALID_CACHED_DATA = 0x00000001;
// Define the FILE_REGION_INPUT structure
[StructLayout(LayoutKind.Sequential)]
public struct FILE_REGION_INPUT
{
public long FileOffset;
public long Length;
public uint DesiredUsage;
public uint Reserved;
}
//Correct struct def
//https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6a8b64a4-ea26-4c5a-8728-c992dd657664
[StructLayout(LayoutKind.Sequential)]
public struct FILE_REGION_OUTPUT
{
public uint Flags;
public uint TotalRegionEntryCount;
public uint RegionEntryCount;
public uint Reserved;
public FILE_REGION_INFO Region;
}
// Define the FILE_REGION_INFO structure
[StructLayout(LayoutKind.Sequential)]
public struct FILE_REGION_INFO
{
public long FileOffset;
public long Length;
public uint DesiredUsage;
public uint Reserved;
}
//Perform file region query
//Based on fsutil file queryvaliddata
//Returns a list of the lengths of the data areas with "no valid data" - can only be one, AFAICT
public List<long> QueryFileRegions(string filePath, long offset, long length)
{
var list = new List<long>();
//In procmon we can see fsutil doesnt end up with an option "non directory file" passed to CreateFile.
//Further tracing with API Monitor revealed fsutil's call to CreateFIle, ends up at NtCreateFile, with an Option parameter of FILE_OPEN_FOR_BACKUP_INTENT
//Passing BackupSemantics here seems to get the trace in procmon looking the same. No "Non Directory File" option
SafeFileHandle fileHandle = new SafeFileHandle(Kernel32_h.CreateFile(filePath,
EFileAccess.FILE_READ_DATA | EFileAccess.FILE_READ_ATTRIBUTES | EFileAccess.SYNCHRONIZE,
EFileShare.Read | EFileShare.Write,
IntPtr.Zero,
ECreationDisposition.OpenExisting,
EFileAttributes.BackupSemantics, IntPtr.Zero), true);
if (Marshal.GetLastWin32Error() != 0)
throw new IOException("Could not open file - " + filePath, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
using (fileHandle)
{
var sizeOutputAndInfo = Marshal.SizeOf(typeof(FILE_REGION_OUTPUT)); // 40. Output header, and first info struct.
var sizeInfo = Marshal.SizeOf(typeof(FILE_REGION_INFO));
int nInBufferSize;
IntPtr lpInBuffer;
if (length == 0 && offset == 0)
{
nInBufferSize = 0;
lpInBuffer = IntPtr.Zero;
}
else
{
FILE_REGION_INPUT regionInput;
regionInput.Length = length;
regionInput.FileOffset = offset;
regionInput.DesiredUsage = FILE_REGION_USAGE_VALID_CACHED_DATA | FILE_REGION_USAGE_VALID_NONCACHED_DATA;//01 is ntfs only and 02 is refs only. I dont know what they mean.
regionInput.Reserved = 0;
nInBufferSize = Marshal.SizeOf(typeof(FILE_REGION_INPUT));
lpInBuffer = Marshal.AllocHGlobal(nInBufferSize);
Marshal.StructureToPtr(regionInput, lpInBuffer, false);
}
int nOutBufferSize = sizeOutputAndInfo + (sizeInfo * 1024);
IntPtr lpOutBuffer = Marshal.AllocHGlobal(nOutBufferSize);
uint bytesReturned;
try
{
bool success = Kernel32_h.DeviceIoControl(fileHandle,
FSCTL_QUERY_FILE_REGIONS,
lpInBuffer, (uint)nInBufferSize,
lpOutBuffer, (uint)nOutBufferSize,
out bytesReturned, IntPtr.Zero);
if (success)
{
FILE_REGION_OUTPUT result = new FILE_REGION_OUTPUT();
result = (FILE_REGION_OUTPUT)Marshal.PtrToStructure(lpOutBuffer, typeof(FILE_REGION_OUTPUT));
//There is only ever going to be 2 regions. And if there are 2 "RegionEntryCount", the first one is never going to be NVD
if (result.Region.DesiredUsage == 0)
list.Add(result.Region.Length);
//First of the next records starts at +40 bytes.
IntPtr lpNextRegion = new IntPtr(lpOutBuffer.ToInt64() + sizeOutputAndInfo);
var bytesRead = sizeOutputAndInfo;
if (bytesReturned > sizeOutputAndInfo)
{
FILE_REGION_INFO nextRegion = (FILE_REGION_INFO)Marshal.PtrToStructure(lpNextRegion, typeof(FILE_REGION_INFO));
if (result.Region.DesiredUsage == 0)
list.Add(nextRegion.Length);
//Untsted code, as I know no way that the system would return more than 2 regions.
//If you write one byte into the middle of an NVD file, the system jhust fill the start with VD.
/*bytesRead += sizeInfo;
while (bytesRead < bytesReturned) {
lpNextRegion = new IntPtr(lpOutBuffer.ToInt64() + sizeInfo);
bytesRead += sizeInfo;
nextRegion = (FILE_REGION_INFO)Marshal.PtrToStructure(lpNextRegion, typeof(FILE_REGION_INFO));
list.Add(nextRegion.Length);
}*/
}
}
else
{
throw new IOException("Could not read file regions - " + filePath, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()));
}
}
finally
{
Marshal.FreeHGlobal(lpInBuffer);
Marshal.FreeHGlobal(lpOutBuffer);
}
}
return list;
}
}
public class Kernel32_h
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode,
IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize,
out uint lpBytesReturned, IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateFile(
string lpFileName,
EFileAccess dwDesiredAccess,
EFileShare dwShareMode,
IntPtr lpSecurityAttributes,
ECreationDisposition dwCreationDisposition,
EFileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
}
SetFileValidData
. – Cursive