Upload FastSeekWpf/Core/MftReader.cs
Browse files- FastSeekWpf/Core/MftReader.cs +108 -47
FastSeekWpf/Core/MftReader.cs
CHANGED
|
@@ -13,20 +13,24 @@ public class MftReader : IDisposable
|
|
| 13 |
private readonly NtfsDrive _drive;
|
| 14 |
private bool _disposed;
|
| 15 |
|
| 16 |
-
private const
|
| 17 |
|
| 18 |
public NtfsDrive Drive => _drive;
|
| 19 |
|
| 20 |
public MftReader(NtfsDrive drive)
|
| 21 |
{
|
| 22 |
_drive = drive;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
_volumeHandle = Win32Api.CreateFileW(
|
| 24 |
drive.DevicePath,
|
| 25 |
Win32Api.GENERIC_READ,
|
| 26 |
Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
|
| 27 |
IntPtr.Zero,
|
| 28 |
Win32Api.OPEN_EXISTING,
|
| 29 |
-
|
| 30 |
IntPtr.Zero);
|
| 31 |
|
| 32 |
if (_volumeHandle == new IntPtr(-1))
|
|
@@ -37,112 +41,132 @@ public class MftReader : IDisposable
|
|
| 37 |
}
|
| 38 |
|
| 39 |
/// <summary>
|
| 40 |
-
/// Direct MFT read: parse boot sector,
|
| 41 |
-
///
|
| 42 |
/// </summary>
|
| 43 |
public ScanResult ScanDirect()
|
| 44 |
{
|
| 45 |
-
//
|
| 46 |
byte[] boot = new byte[512];
|
| 47 |
bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out uint br, IntPtr.Zero);
|
| 48 |
if (!ok || br < 512)
|
| 49 |
-
|
| 50 |
-
int err = Marshal.GetLastWin32Error();
|
| 51 |
-
throw new IOException($"Failed to read boot sector: {Win32Error(err)}");
|
| 52 |
-
}
|
| 53 |
|
| 54 |
-
// Validate NTFS signature at offset 3: "NTFS "
|
| 55 |
if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
|
| 56 |
throw new IOException("Not an NTFS volume (missing NTFS signature)");
|
| 57 |
|
| 58 |
-
ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(
|
| 59 |
byte sectorsPerCluster = boot[0x0D];
|
| 60 |
int clusterSize = bytesPerSector * sectorsPerCluster;
|
| 61 |
|
| 62 |
-
|
| 63 |
-
long mftStartLcn = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan<byte>(boot, 0x30, 8));
|
| 64 |
if (mftStartLcn <= 0)
|
| 65 |
throw new IOException($"Invalid MFT start LCN: {mftStartLcn}");
|
| 66 |
|
|
|
|
|
|
|
|
|
|
| 67 |
long mftByteOffset = mftStartLcn * clusterSize;
|
| 68 |
-
Logger.Log($"Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, mftLcn={mftStartLcn},
|
| 69 |
|
| 70 |
-
// Seek to MFT
|
| 71 |
ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
|
| 72 |
if (!ok)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
{
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
}
|
| 77 |
-
|
| 78 |
-
// MFT record size: raw byte at 0x40
|
| 79 |
-
sbyte raw = (sbyte)boot[0x40];
|
| 80 |
-
int recordSize;
|
| 81 |
-
if (raw > 0)
|
| 82 |
-
recordSize = raw * clusterSize;
|
| 83 |
else
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
var records = new List<CompactRecord>();
|
| 89 |
var nameData = new List<char>();
|
| 90 |
-
byte[]
|
| 91 |
ulong mftIndex = 0;
|
| 92 |
-
|
| 93 |
-
|
|
|
|
| 94 |
|
| 95 |
-
while (
|
| 96 |
{
|
| 97 |
-
|
|
|
|
| 98 |
if (!ok)
|
| 99 |
{
|
| 100 |
int err = Marshal.GetLastWin32Error();
|
| 101 |
-
if (err == 38) // ERROR_HANDLE_EOF
|
| 102 |
{
|
| 103 |
-
Logger.Log("MFT read:
|
| 104 |
break;
|
| 105 |
}
|
| 106 |
-
|
|
|
|
| 107 |
}
|
| 108 |
if (bytesRead == 0)
|
| 109 |
{
|
| 110 |
-
Logger.Log("
|
| 111 |
break;
|
| 112 |
}
|
| 113 |
|
| 114 |
-
|
| 115 |
-
int total = leftover + (int)bytesRead;
|
| 116 |
int offset = 0;
|
|
|
|
| 117 |
|
| 118 |
while (offset + recordSize <= total)
|
| 119 |
{
|
| 120 |
-
var span = new ReadOnlySpan<byte>(
|
| 121 |
if (ApplyFixup(span, recordSize))
|
| 122 |
{
|
| 123 |
ParseFileRecord(span, mftIndex, records, nameData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
| 125 |
mftIndex++;
|
| 126 |
offset += recordSize;
|
| 127 |
-
}
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
-
Logger.Log($"MFT
|
| 135 |
return new ScanResult(records, nameData);
|
| 136 |
}
|
| 137 |
|
| 138 |
/// <summary>
|
| 139 |
-
/// Fallback FSCTL_ENUM_USN_DATA scan.
|
|
|
|
| 140 |
/// </summary>
|
| 141 |
public ScanResult Scan()
|
| 142 |
{
|
| 143 |
var records = new List<CompactRecord>();
|
| 144 |
var nameData = new List<char>();
|
| 145 |
-
byte[] buffer = new byte[4 * 1024 * 1024];
|
| 146 |
|
| 147 |
var enumData = new MFT_ENUM_DATA_V0
|
| 148 |
{
|
|
@@ -179,15 +203,12 @@ public class MftReader : IDisposable
|
|
| 179 |
Logger.Log($"FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
|
| 180 |
break;
|
| 181 |
}
|
| 182 |
-
|
| 183 |
-
// Check if it's a buffer-too-small error
|
| 184 |
if (error == 122) // ERROR_INSUFFICIENT_BUFFER
|
| 185 |
{
|
| 186 |
Logger.Log("FSCTL_ENUM_USN_DATA: insufficient buffer, retrying with larger buffer");
|
| 187 |
buffer = new byte[buffer.Length * 2];
|
| 188 |
continue;
|
| 189 |
}
|
| 190 |
-
|
| 191 |
throw new IOException(
|
| 192 |
$"FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)}. " +
|
| 193 |
$"StartFileReferenceNumber={enumData.StartFileReferenceNumber}");
|
|
@@ -248,6 +269,46 @@ public class MftReader : IDisposable
|
|
| 248 |
return new ScanResult(records, nameData);
|
| 249 |
}
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
private static bool ApplyFixup(ReadOnlySpan<byte> record, int recordSize)
|
| 252 |
{
|
| 253 |
if (record.Length < 48 || record[0] != 'F' || record[1] != 'I' || record[2] != 'L' || record[3] != 'E')
|
|
|
|
| 13 |
private readonly NtfsDrive _drive;
|
| 14 |
private bool _disposed;
|
| 15 |
|
| 16 |
+
private const int ChunkSize = 256 * 1024; // 256KB β large enough for speed, small enough for memory
|
| 17 |
|
| 18 |
public NtfsDrive Drive => _drive;
|
| 19 |
|
| 20 |
public MftReader(NtfsDrive drive)
|
| 21 |
{
|
| 22 |
_drive = drive;
|
| 23 |
+
// NOTE: FILE_FLAG_NO_BUFFERING removed. It requires sector-aligned buffers AND
|
| 24 |
+
// aligned file offsets, and .NET arrays are not guaranteed aligned. The Rust
|
| 25 |
+
// code likely opens the volume without NO_BUFFERING. Buffered reads are fast
|
| 26 |
+
// enough since we now read exactly the MFT size, not the whole volume.
|
| 27 |
_volumeHandle = Win32Api.CreateFileW(
|
| 28 |
drive.DevicePath,
|
| 29 |
Win32Api.GENERIC_READ,
|
| 30 |
Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
|
| 31 |
IntPtr.Zero,
|
| 32 |
Win32Api.OPEN_EXISTING,
|
| 33 |
+
0, // FILE_ATTRIBUTE_NORMAL β buffered, cached reads
|
| 34 |
IntPtr.Zero);
|
| 35 |
|
| 36 |
if (_volumeHandle == new IntPtr(-1))
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
/// <summary>
|
| 44 |
+
/// Direct MFT read: parse boot sector, seek to MFT start, read first record
|
| 45 |
+
/// to discover MFT $DATA size, then read exactly that many bytes.
|
| 46 |
/// </summary>
|
| 47 |
public ScanResult ScanDirect()
|
| 48 |
{
|
| 49 |
+
// ββ 1. Boot sector ββ
|
| 50 |
byte[] boot = new byte[512];
|
| 51 |
bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out uint br, IntPtr.Zero);
|
| 52 |
if (!ok || br < 512)
|
| 53 |
+
throw new IOException($"Failed to read boot sector: {Win32Error(Marshal.GetLastWin32Error())}");
|
|
|
|
|
|
|
|
|
|
| 54 |
|
|
|
|
| 55 |
if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
|
| 56 |
throw new IOException("Not an NTFS volume (missing NTFS signature)");
|
| 57 |
|
| 58 |
+
ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2));
|
| 59 |
byte sectorsPerCluster = boot[0x0D];
|
| 60 |
int clusterSize = bytesPerSector * sectorsPerCluster;
|
| 61 |
|
| 62 |
+
long mftStartLcn = BinaryPrimitives.ReadInt64LittleEndian(boot.AsSpan(0x30, 8));
|
|
|
|
| 63 |
if (mftStartLcn <= 0)
|
| 64 |
throw new IOException($"Invalid MFT start LCN: {mftStartLcn}");
|
| 65 |
|
| 66 |
+
sbyte raw = (sbyte)boot[0x40];
|
| 67 |
+
int recordSize = raw < 0 ? 1 << (-raw) : raw * clusterSize;
|
| 68 |
+
|
| 69 |
long mftByteOffset = mftStartLcn * clusterSize;
|
| 70 |
+
Logger.Log($"Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, mftLcn={mftStartLcn}, recordSize={recordSize}");
|
| 71 |
|
| 72 |
+
// ββ 2. Seek to MFT and read first record ($MFT itself) ββ
|
| 73 |
ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
|
| 74 |
if (!ok)
|
| 75 |
+
throw new IOException($"Seek to MFT offset {mftByteOffset} failed: {Win32Error(Marshal.GetLastWin32Error())}");
|
| 76 |
+
|
| 77 |
+
byte[] firstRecord = new byte[recordSize];
|
| 78 |
+
ok = Win32Api.ReadFile(_volumeHandle, firstRecord, (uint)recordSize, out br, IntPtr.Zero);
|
| 79 |
+
if (!ok || br < recordSize)
|
| 80 |
+
throw new IOException($"Failed to read first MFT record: {Win32Error(Marshal.GetLastWin32Error())}");
|
| 81 |
+
|
| 82 |
+
// ββ 3. Parse MFT total data size from its own $DATA attribute ββ
|
| 83 |
+
long mftDataSize = ParseMftDataSize(firstRecord);
|
| 84 |
+
if (mftDataSize <= 0)
|
| 85 |
{
|
| 86 |
+
Logger.Log("WARNING: Could not parse MFT $DATA size; using 2GB fallback. " +
|
| 87 |
+
"This may read past the end of the MFT.");
|
| 88 |
+
mftDataSize = 2L * 1024 * 1024 * 1024;
|
| 89 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
else
|
| 91 |
+
{
|
| 92 |
+
Logger.Log($"MFT data size from $DATA attribute: {mftDataSize:N0} bytes ({mftDataSize / 1024.0 / 1024.0:F1} MB)");
|
| 93 |
+
}
|
| 94 |
|
| 95 |
+
// ββ 4. Reset to MFT start and read exactly mftDataSize bytes ββ
|
| 96 |
+
ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
|
| 97 |
+
if (!ok)
|
| 98 |
+
throw new IOException($"Seek back to MFT start failed: {Win32Error(Marshal.GetLastWin32Error())}");
|
| 99 |
|
| 100 |
var records = new List<CompactRecord>();
|
| 101 |
var nameData = new List<char>();
|
| 102 |
+
byte[] chunk = new byte[ChunkSize];
|
| 103 |
ulong mftIndex = 0;
|
| 104 |
+
long bytesRemaining = mftDataSize;
|
| 105 |
+
int consecutiveInvalid = 0;
|
| 106 |
+
const int MaxConsecutiveInvalid = 50;
|
| 107 |
|
| 108 |
+
while (bytesRemaining > 0 && consecutiveInvalid < MaxConsecutiveInvalid)
|
| 109 |
{
|
| 110 |
+
int toRead = (int)Math.Min(ChunkSize, bytesRemaining);
|
| 111 |
+
ok = Win32Api.ReadFile(_volumeHandle, chunk, (uint)toRead, out uint bytesRead, IntPtr.Zero);
|
| 112 |
if (!ok)
|
| 113 |
{
|
| 114 |
int err = Marshal.GetLastWin32Error();
|
| 115 |
+
if (err == 38) // ERROR_HANDLE_EOF
|
| 116 |
{
|
| 117 |
+
Logger.Log("MFT read: EOF reached");
|
| 118 |
break;
|
| 119 |
}
|
| 120 |
+
Logger.Log($"ReadFile failed at offset {mftByteOffset + (mftDataSize - bytesRemaining)}: {Win32Error(err)} β stopping");
|
| 121 |
+
break;
|
| 122 |
}
|
| 123 |
if (bytesRead == 0)
|
| 124 |
{
|
| 125 |
+
Logger.Log("ReadFile returned 0 bytes β stopping");
|
| 126 |
break;
|
| 127 |
}
|
| 128 |
|
| 129 |
+
bytesRemaining -= bytesRead;
|
|
|
|
| 130 |
int offset = 0;
|
| 131 |
+
int total = (int)bytesRead;
|
| 132 |
|
| 133 |
while (offset + recordSize <= total)
|
| 134 |
{
|
| 135 |
+
var span = new ReadOnlySpan<byte>(chunk, offset, recordSize);
|
| 136 |
if (ApplyFixup(span, recordSize))
|
| 137 |
{
|
| 138 |
ParseFileRecord(span, mftIndex, records, nameData);
|
| 139 |
+
consecutiveInvalid = 0;
|
| 140 |
+
}
|
| 141 |
+
else
|
| 142 |
+
{
|
| 143 |
+
consecutiveInvalid++;
|
| 144 |
}
|
| 145 |
mftIndex++;
|
| 146 |
offset += recordSize;
|
|
|
|
| 147 |
|
| 148 |
+
if (consecutiveInvalid >= MaxConsecutiveInvalid)
|
| 149 |
+
{
|
| 150 |
+
Logger.Log($"Stopping after {MaxConsecutiveInvalid} consecutive invalid records at index {mftIndex}");
|
| 151 |
+
bytesRemaining = 0;
|
| 152 |
+
break;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
}
|
| 156 |
|
| 157 |
+
Logger.Log($"MFT scan complete: {records.Count:N0} records from {mftIndex:N0} positions ({mftDataSize - bytesRemaining:N0} bytes read)");
|
| 158 |
return new ScanResult(records, nameData);
|
| 159 |
}
|
| 160 |
|
| 161 |
/// <summary>
|
| 162 |
+
/// Fallback FSCTL_ENUM_USN_DATA scan. Kept for parity but direct scan should
|
| 163 |
+
/// be preferred β it is faster and doesn't require a USN journal.
|
| 164 |
/// </summary>
|
| 165 |
public ScanResult Scan()
|
| 166 |
{
|
| 167 |
var records = new List<CompactRecord>();
|
| 168 |
var nameData = new List<char>();
|
| 169 |
+
byte[] buffer = new byte[4 * 1024 * 1024];
|
| 170 |
|
| 171 |
var enumData = new MFT_ENUM_DATA_V0
|
| 172 |
{
|
|
|
|
| 203 |
Logger.Log($"FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
|
| 204 |
break;
|
| 205 |
}
|
|
|
|
|
|
|
| 206 |
if (error == 122) // ERROR_INSUFFICIENT_BUFFER
|
| 207 |
{
|
| 208 |
Logger.Log("FSCTL_ENUM_USN_DATA: insufficient buffer, retrying with larger buffer");
|
| 209 |
buffer = new byte[buffer.Length * 2];
|
| 210 |
continue;
|
| 211 |
}
|
|
|
|
| 212 |
throw new IOException(
|
| 213 |
$"FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)}. " +
|
| 214 |
$"StartFileReferenceNumber={enumData.StartFileReferenceNumber}");
|
|
|
|
| 269 |
return new ScanResult(records, nameData);
|
| 270 |
}
|
| 271 |
|
| 272 |
+
// ββ Helpers ββ
|
| 273 |
+
|
| 274 |
+
/// <summary>
|
| 275 |
+
/// Parses the $DATA attribute (0x80) from the $MFT record itself (record 0)
|
| 276 |
+
/// to discover the total size of the MFT data on disk.
|
| 277 |
+
/// </summary>
|
| 278 |
+
private static long ParseMftDataSize(ReadOnlySpan<byte> record)
|
| 279 |
+
{
|
| 280 |
+
if (record.Length < 48 || record[0] != 'F' || record[1] != 'I' || record[2] != 'L' || record[3] != 'E')
|
| 281 |
+
return -1;
|
| 282 |
+
|
| 283 |
+
// Apply fixup first so the record is valid
|
| 284 |
+
if (!ApplyFixup(record, record.Length))
|
| 285 |
+
return -1;
|
| 286 |
+
|
| 287 |
+
ushort firstAttr = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x14, 2));
|
| 288 |
+
int aoff = firstAttr;
|
| 289 |
+
|
| 290 |
+
while (aoff + 0x40 <= record.Length)
|
| 291 |
+
{
|
| 292 |
+
uint atype = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff, 4));
|
| 293 |
+
if (atype == 0xFFFFFFFF) break;
|
| 294 |
+
|
| 295 |
+
uint alen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 4, 4));
|
| 296 |
+
if (alen == 0 || aoff + alen > record.Length) break;
|
| 297 |
+
|
| 298 |
+
// 0x80 = $DATA, non-resident flag = 1
|
| 299 |
+
if (atype == 0x80 && record[aoff + 8] == 1)
|
| 300 |
+
{
|
| 301 |
+
// Non-resident header: DataSize (RealSize) is at offset 0x30 from attr start
|
| 302 |
+
long dataSize = BinaryPrimitives.ReadInt64LittleEndian(record.Slice(aoff + 0x30, 8));
|
| 303 |
+
return dataSize;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
aoff += (int)alen;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
return -1;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
private static bool ApplyFixup(ReadOnlySpan<byte> record, int recordSize)
|
| 313 |
{
|
| 314 |
if (record.Length < 48 || record[0] != 'F' || record[1] != 'I' || record[2] != 'L' || record[3] != 'E')
|