CRITICAL FIX: Use Win32Api.ERROR_HANDLE_EOF (raw 38) instead of literal HRESULT 0x80070026. GetLastWin32Error() returns raw Win32 codes, not HRESULTs. This was causing EOF to never be detected, making the scan loop run forever on garbage data and return 0 records.
Browse files- FastSeekWpf/Core/MftReader.cs +17 -38
FastSeekWpf/Core/MftReader.cs
CHANGED
|
@@ -9,7 +9,7 @@ namespace FastSeekWpf.Core;
|
|
| 9 |
|
| 10 |
public class MftReader : IDisposable
|
| 11 |
{
|
| 12 |
-
private readonly IntPtr
|
| 13 |
private readonly NtfsDrive _drive;
|
| 14 |
private bool _disposed;
|
| 15 |
|
|
@@ -21,7 +21,7 @@ public class MftReader : IDisposable
|
|
| 21 |
public MftReader(NtfsDrive drive)
|
| 22 |
{
|
| 23 |
_drive = drive;
|
| 24 |
-
|
| 25 |
drive.DevicePath,
|
| 26 |
Win32Api.GENERIC_READ,
|
| 27 |
Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
|
|
@@ -30,17 +30,14 @@ public class MftReader : IDisposable
|
|
| 30 |
Win32Api.FILE_FLAG_BACKUP_SEMANTICS,
|
| 31 |
IntPtr.Zero);
|
| 32 |
|
| 33 |
-
if (
|
| 34 |
{
|
| 35 |
int err = Marshal.GetLastWin32Error();
|
| 36 |
throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
-
//
|
| 41 |
-
// Try direct first, fall back to FSCTL (matches Rust main.rs exactly)
|
| 42 |
-
// ------------------------------------------------------------------
|
| 43 |
-
|
| 44 |
public (ScanResult scan, string method) ScanAny()
|
| 45 |
{
|
| 46 |
var direct = ScanDirect();
|
|
@@ -49,10 +46,7 @@ public class MftReader : IDisposable
|
|
| 49 |
return (Scan(), "ioctl");
|
| 50 |
}
|
| 51 |
|
| 52 |
-
//
|
| 53 |
-
// Primary: direct $MFT file read (matches Rust scan_direct())
|
| 54 |
-
// ------------------------------------------------------------------
|
| 55 |
-
|
| 56 |
public ScanResult? ScanDirect()
|
| 57 |
{
|
| 58 |
int? recordSize = ReadMftRecordSize();
|
|
@@ -113,13 +107,13 @@ public class MftReader : IDisposable
|
|
| 113 |
offset += recordSize.Value;
|
| 114 |
}
|
| 115 |
|
| 116 |
-
// Align down to record boundary
|
| 117 |
offset = total - (total % recordSize.Value);
|
| 118 |
|
| 119 |
leftover = total - offset;
|
| 120 |
if (leftover > 0)
|
| 121 |
{
|
| 122 |
-
// Copy tail to front
|
| 123 |
Buffer.BlockCopy(buffer, offset, buffer, 0, leftover);
|
| 124 |
}
|
| 125 |
}
|
|
@@ -133,11 +127,7 @@ public class MftReader : IDisposable
|
|
| 133 |
}
|
| 134 |
}
|
| 135 |
|
| 136 |
-
//
|
| 137 |
-
// Fallback: FSCTL_ENUM_USN_DATA (4 MB buffer)
|
| 138 |
-
// Matches Rust scan() exactly.
|
| 139 |
-
// ------------------------------------------------------------------
|
| 140 |
-
|
| 141 |
public ScanResult Scan()
|
| 142 |
{
|
| 143 |
var records = new List<CompactRecord>();
|
|
@@ -157,16 +147,14 @@ public class MftReader : IDisposable
|
|
| 157 |
|
| 158 |
try
|
| 159 |
{
|
| 160 |
-
int iterations = 0;
|
| 161 |
while (true)
|
| 162 |
{
|
| 163 |
-
iterations++;
|
| 164 |
IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length);
|
| 165 |
try
|
| 166 |
{
|
| 167 |
uint bytesReturned = 0;
|
| 168 |
bool ok = Win32Api.DeviceIoControl(
|
| 169 |
-
|
| 170 |
Win32Api.FSCTL_ENUM_USN_DATA,
|
| 171 |
enumDataPtr,
|
| 172 |
(uint)enumDataSize,
|
|
@@ -178,13 +166,13 @@ public class MftReader : IDisposable
|
|
| 178 |
if (!ok)
|
| 179 |
{
|
| 180 |
int error = Marshal.GetLastWin32Error();
|
| 181 |
-
|
| 182 |
-
if (
|
| 183 |
{
|
| 184 |
-
Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF
|
| 185 |
break;
|
| 186 |
}
|
| 187 |
-
if (error ==
|
| 188 |
{
|
| 189 |
Logger.Log($"[{Drive.Letter}] FSCTL buffer too small ({buffer.Length}), retrying");
|
| 190 |
buffer = new byte[buffer.Length * 2];
|
|
@@ -249,13 +237,9 @@ public class MftReader : IDisposable
|
|
| 249 |
return new ScanResult(records, nameData);
|
| 250 |
}
|
| 251 |
|
| 252 |
-
//
|
| 253 |
-
// NTFS helpers (match Rust exactly)
|
| 254 |
-
// ------------------------------------------------------------------
|
| 255 |
-
|
| 256 |
private int? ReadMftRecordSize()
|
| 257 |
{
|
| 258 |
-
// Seek to beginning of volume
|
| 259 |
Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
|
| 260 |
|
| 261 |
byte[] boot = new byte[512];
|
|
@@ -278,10 +262,7 @@ public class MftReader : IDisposable
|
|
| 278 |
return recordSize;
|
| 279 |
}
|
| 280 |
|
| 281 |
-
//
|
| 282 |
-
/// Apply NTFS multi-sector fixup. Returns false if record is invalid.
|
| 283 |
-
/// CRITICAL: restores bytes from fixup array back to sector ends (matches Rust exactly).
|
| 284 |
-
/// </summary>
|
| 285 |
private static bool ApplyFixup(byte[] record, int offset, int recordSize)
|
| 286 |
{
|
| 287 |
if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I'
|
|
@@ -305,7 +286,7 @@ public class MftReader : IDisposable
|
|
| 305 |
if (record[offset + end] != check0 || record[offset + end + 1] != check1)
|
| 306 |
return false;
|
| 307 |
|
| 308 |
-
// RESTORE real bytes from fixup array
|
| 309 |
record[offset + end] = record[offset + fixupOff + i * 2];
|
| 310 |
record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
|
| 311 |
}
|
|
@@ -313,9 +294,7 @@ public class MftReader : IDisposable
|
|
| 313 |
return true;
|
| 314 |
}
|
| 315 |
|
| 316 |
-
//
|
| 317 |
-
/// Parse one MFT FILE record (matches Rust parse_file_record exactly).
|
| 318 |
-
/// </summary>
|
| 319 |
private static void ParseFileRecord(
|
| 320 |
ReadOnlySpan<byte> record,
|
| 321 |
ulong mftIndex,
|
|
|
|
| 9 |
|
| 10 |
public class MftReader : IDisposable
|
| 11 |
{
|
| 12 |
+
private readonly IntPtr _handle;
|
| 13 |
private readonly NtfsDrive _drive;
|
| 14 |
private bool _disposed;
|
| 15 |
|
|
|
|
| 21 |
public MftReader(NtfsDrive drive)
|
| 22 |
{
|
| 23 |
_drive = drive;
|
| 24 |
+
_handle = Win32Api.CreateFileW(
|
| 25 |
drive.DevicePath,
|
| 26 |
Win32Api.GENERIC_READ,
|
| 27 |
Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
|
|
|
|
| 30 |
Win32Api.FILE_FLAG_BACKUP_SEMANTICS,
|
| 31 |
IntPtr.Zero);
|
| 32 |
|
| 33 |
+
if (_handle == new IntPtr(-1))
|
| 34 |
{
|
| 35 |
int err = Marshal.GetLastWin32Error();
|
| 36 |
throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
| 40 |
+
// Try direct first, fall back to FSCTL — matches Rust main.rs exactly
|
|
|
|
|
|
|
|
|
|
| 41 |
public (ScanResult scan, string method) ScanAny()
|
| 42 |
{
|
| 43 |
var direct = ScanDirect();
|
|
|
|
| 46 |
return (Scan(), "ioctl");
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// Primary: direct $MFT file read — matches Rust scan_direct()
|
|
|
|
|
|
|
|
|
|
| 50 |
public ScanResult? ScanDirect()
|
| 51 |
{
|
| 52 |
int? recordSize = ReadMftRecordSize();
|
|
|
|
| 107 |
offset += recordSize.Value;
|
| 108 |
}
|
| 109 |
|
| 110 |
+
// Align down to record boundary — matches Rust offset = total - (total % record_size)
|
| 111 |
offset = total - (total % recordSize.Value);
|
| 112 |
|
| 113 |
leftover = total - offset;
|
| 114 |
if (leftover > 0)
|
| 115 |
{
|
| 116 |
+
// Copy tail to front — matches Rust std::ptr::copy
|
| 117 |
Buffer.BlockCopy(buffer, offset, buffer, 0, leftover);
|
| 118 |
}
|
| 119 |
}
|
|
|
|
| 127 |
}
|
| 128 |
}
|
| 129 |
|
| 130 |
+
// Fallback: FSCTL_ENUM_USN_DATA (4 MB buffer) — matches Rust scan() exactly
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
public ScanResult Scan()
|
| 132 |
{
|
| 133 |
var records = new List<CompactRecord>();
|
|
|
|
| 147 |
|
| 148 |
try
|
| 149 |
{
|
|
|
|
| 150 |
while (true)
|
| 151 |
{
|
|
|
|
| 152 |
IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length);
|
| 153 |
try
|
| 154 |
{
|
| 155 |
uint bytesReturned = 0;
|
| 156 |
bool ok = Win32Api.DeviceIoControl(
|
| 157 |
+
_handle,
|
| 158 |
Win32Api.FSCTL_ENUM_USN_DATA,
|
| 159 |
enumDataPtr,
|
| 160 |
(uint)enumDataSize,
|
|
|
|
| 166 |
if (!ok)
|
| 167 |
{
|
| 168 |
int error = Marshal.GetLastWin32Error();
|
| 169 |
+
// CRITICAL: GetLastWin32Error returns RAW Win32 error (38), NOT HRESULT (0x80070026)
|
| 170 |
+
if (error == Win32Api.ERROR_HANDLE_EOF)
|
| 171 |
{
|
| 172 |
+
Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF");
|
| 173 |
break;
|
| 174 |
}
|
| 175 |
+
if (error == Win32Api.ERROR_INSUFFICIENT_BUFFER)
|
| 176 |
{
|
| 177 |
Logger.Log($"[{Drive.Letter}] FSCTL buffer too small ({buffer.Length}), retrying");
|
| 178 |
buffer = new byte[buffer.Length * 2];
|
|
|
|
| 237 |
return new ScanResult(records, nameData);
|
| 238 |
}
|
| 239 |
|
| 240 |
+
// Read MFT record size from NTFS boot sector — matches Rust read_mft_record_size()
|
|
|
|
|
|
|
|
|
|
| 241 |
private int? ReadMftRecordSize()
|
| 242 |
{
|
|
|
|
| 243 |
Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
|
| 244 |
|
| 245 |
byte[] boot = new byte[512];
|
|
|
|
| 262 |
return recordSize;
|
| 263 |
}
|
| 264 |
|
| 265 |
+
// Apply NTFS multi-sector fixup — matches Rust apply_fixup() exactly
|
|
|
|
|
|
|
|
|
|
| 266 |
private static bool ApplyFixup(byte[] record, int offset, int recordSize)
|
| 267 |
{
|
| 268 |
if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I'
|
|
|
|
| 286 |
if (record[offset + end] != check0 || record[offset + end + 1] != check1)
|
| 287 |
return false;
|
| 288 |
|
| 289 |
+
// RESTORE real bytes from fixup array — matches Rust exactly
|
| 290 |
record[offset + end] = record[offset + fixupOff + i * 2];
|
| 291 |
record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
|
| 292 |
}
|
|
|
|
| 294 |
return true;
|
| 295 |
}
|
| 296 |
|
| 297 |
+
// Parse one MFT FILE record — matches Rust parse_file_record() exactly
|
|
|
|
|
|
|
| 298 |
private static void ParseFileRecord(
|
| 299 |
ReadOnlySpan<byte> record,
|
| 300 |
ulong mftIndex,
|