anshdadhich commited on
Commit
b04a10b
Β·
verified Β·
1 Parent(s): 9961292

Upload FastSeekWpf/Core/MftReader.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/MftReader.cs +126 -154
FastSeekWpf/Core/MftReader.cs CHANGED
@@ -9,20 +9,19 @@ namespace FastSeekWpf.Core;
9
 
10
  public class MftReader : IDisposable
11
  {
 
12
  private readonly IntPtr _volumeHandle;
13
  private readonly NtfsDrive _drive;
14
  private bool _disposed;
15
 
 
 
 
16
  public NtfsDrive Drive => _drive;
17
 
18
  public MftReader(NtfsDrive drive)
19
  {
20
  _drive = drive;
21
-
22
- // FILE_FLAG_BACKUP_SEMANTICS is REQUIRED for opening a volume device
23
- // for direct MFT reads. FILE_FLAG_NO_BUFFERING is removed because .NET
24
- // arrays are not guaranteed sector-aligned, and unaligned reads with
25
- // NO_BUFFERING hang forever or return ERROR_INVALID_PARAMETER.
26
  _volumeHandle = Win32Api.CreateFileW(
27
  drive.DevicePath,
28
  Win32Api.GENERIC_READ,
@@ -37,134 +36,92 @@ public class MftReader : IDisposable
37
  int err = Marshal.GetLastWin32Error();
38
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)}");
39
  }
40
-
41
- Logger.Log($"Volume {drive.DevicePath} opened OK, handle=0x{_volumeHandle:X}");
42
  }
43
 
 
 
 
 
44
  /// <summary>
45
- /// Direct MFT read matching the Rust implementation.
 
46
  /// </summary>
47
- public ScanResult ScanDirect()
48
  {
49
- Logger.Log($"[{Drive.Letter}] Reading boot sector...");
50
-
51
- // ── 1. Boot sector ──
52
- byte[] boot = new byte[512];
53
- bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out uint br, IntPtr.Zero);
54
- if (!ok || br < 512)
55
- {
56
- int err = Marshal.GetLastWin32Error();
57
- throw new IOException($"Failed to read boot sector: {Win32Error(err)}");
58
- }
59
 
60
- // Validate NTFS signature at offset 3: "NTFS "
61
- if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
62
- throw new IOException("Not an NTFS volume (missing NTFS signature)");
63
-
64
- ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2));
65
- byte sectorsPerCluster = boot[0x0D];
66
- int clusterSize = bytesPerSector * sectorsPerCluster;
67
-
68
- long mftStartLcn = BinaryPrimitives.ReadInt64LittleEndian(boot.AsSpan(0x30, 8));
69
- if (mftStartLcn <= 0)
70
- throw new IOException($"Invalid MFT start LCN: {mftStartLcn}");
71
 
72
- sbyte raw = (sbyte)boot[0x40];
73
- int recordSize = raw < 0 ? 1 << (-raw) : raw * clusterSize;
74
-
75
- long mftByteOffset = mftStartLcn * clusterSize;
76
- Logger.Log($"[{Drive.Letter}] Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, mftLcn={mftStartLcn}, recordSize={recordSize}, offset={mftByteOffset}");
77
-
78
- // ── 2. Seek to MFT ──
79
- ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
80
- if (!ok)
81
- throw new IOException($"Seek to MFT offset {mftByteOffset} failed: {Win32Error(Marshal.GetLastWin32Error())}");
82
 
83
- // ── 3. Read first record ($MFT itself) to get data size ──
84
- Logger.Log($"[{Drive.Letter}] Reading first MFT record to find $DATA size...");
85
- byte[] firstRecord = new byte[recordSize];
86
- ok = Win32Api.ReadFile(_volumeHandle, firstRecord, (uint)recordSize, out br, IntPtr.Zero);
87
- if (!ok || br < recordSize)
88
- throw new IOException($"Failed to read first MFT record: {Win32Error(Marshal.GetLastWin32Error())}");
89
 
90
- long mftDataSize = ParseMftDataSize(firstRecord);
91
- if (mftDataSize <= 0)
92
- {
93
- Logger.Log($"[{Drive.Letter}] WARNING: Could not parse MFT $DATA size; using 2GB fallback.");
94
- mftDataSize = 2L * 1024 * 1024 * 1024;
95
- }
96
- else
97
  {
98
- Logger.Log($"[{Drive.Letter}] MFT data size: {mftDataSize:N0} bytes ({mftDataSize / 1024.0 / 1024.0:F1} MB)");
99
- }
100
-
101
- // ── 4. Seek back to MFT start and read all records ──
102
- ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
103
- if (!ok)
104
- throw new IOException($"Seek back to MFT start failed: {Win32Error(Marshal.GetLastWin32Error())}");
105
 
106
- var records = new List<CompactRecord>();
107
- var nameData = new List<char>();
108
- byte[] buffer = new byte[recordSize]; // read one record at a time for safety
109
- ulong mftIndex = 0;
110
- long bytesRemaining = mftDataSize;
111
- int consecutiveInvalid = 0;
112
- const int MaxConsecutiveInvalid = 50;
113
 
114
- Logger.Log($"[{Drive.Letter}] Starting MFT read loop...");
 
115
 
116
- while (bytesRemaining > 0 && consecutiveInvalid < MaxConsecutiveInvalid)
117
- {
118
- ok = Win32Api.ReadFile(_volumeHandle, buffer, (uint)recordSize, out br, IntPtr.Zero);
119
- if (!ok)
120
- {
121
- int err = Marshal.GetLastWin32Error();
122
- if (err == 38) // ERROR_HANDLE_EOF
123
  {
124
- Logger.Log($"[{Drive.Letter}] MFT read: EOF reached");
125
- break;
 
 
 
 
 
 
126
  }
127
- Logger.Log($"[{Drive.Letter}] ReadFile failed at offset {mftByteOffset + (mftDataSize - bytesRemaining)}: {Win32Error(err)} β€” stopping");
128
- break;
129
- }
130
- if (br == 0)
131
- {
132
- Logger.Log($"[{Drive.Letter}] ReadFile returned 0 bytes β€” stopping");
133
- break;
134
- }
135
 
136
- bytesRemaining -= br;
 
137
 
138
- if (ApplyFixup(buffer, recordSize))
139
- {
140
- ParseFileRecord(buffer.AsSpan(), mftIndex, records, nameData);
141
- consecutiveInvalid = 0;
142
- }
143
- else
144
- {
145
- consecutiveInvalid++;
146
  }
147
 
148
- mftIndex++;
149
-
150
- if (mftIndex % 10000 == 0)
151
- Logger.Log($"[{Drive.Letter}] Progress: {mftIndex:N0} records, {records.Count:N0} valid, {bytesRemaining:N0} bytes remaining");
 
 
152
  }
153
-
154
- Logger.Log($"[{Drive.Letter}] MFT scan complete: {records.Count:N0} records from {mftIndex:N0} positions");
155
- return new ScanResult(records, nameData);
156
  }
157
 
158
- /// <summary>
159
- /// Fallback FSCTL_ENUM_USN_DATA scan.
160
- /// </summary>
 
161
  public ScanResult Scan()
162
  {
163
- Logger.Log($"[{Drive.Letter}] Starting FSCTL fallback scan...");
164
-
165
  var records = new List<CompactRecord>();
166
  var nameData = new List<char>();
167
- byte[] buffer = new byte[4 * 1024 * 1024];
168
 
169
  var enumData = new MFT_ENUM_DATA_V0
170
  {
@@ -203,18 +160,17 @@ public class MftReader : IDisposable
203
  }
204
  if (error == 122) // ERROR_INSUFFICIENT_BUFFER
205
  {
206
- Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: insufficient buffer, retrying with larger buffer");
207
  buffer = new byte[buffer.Length * 2];
208
  continue;
209
  }
210
- throw new IOException(
211
- $"FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)}. " +
212
- $"StartFileReferenceNumber={enumData.StartFileReferenceNumber}");
213
  }
214
 
215
  if (bytesReturned <= 8)
216
  {
217
- Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: {bytesReturned} bytes returned, stopping");
218
  break;
219
  }
220
 
@@ -228,7 +184,8 @@ public class MftReader : IDisposable
228
  IntPtr recordPtr = IntPtr.Add(bufferPtr, offset);
229
  var record = Marshal.PtrToStructure<USN_RECORD_V2>(recordPtr);
230
 
231
- if (record.RecordLength == 0) break;
 
232
 
233
  int nameOffset = offset + (int)record.FileNameOffset;
234
  int nameLen = record.FileNameLength / 2;
@@ -249,7 +206,7 @@ public class MftReader : IDisposable
249
  IsDir = (record.FileAttributes & 0x10) != 0
250
  });
251
 
252
- offset += (int)record.RecordLength;
253
  }
254
  }
255
  finally
@@ -263,72 +220,87 @@ public class MftReader : IDisposable
263
  Marshal.FreeHGlobal(enumDataPtr);
264
  }
265
 
266
- Logger.Log($"[{Drive.Letter}] FSCTL scan complete: {records.Count} records");
267
  return new ScanResult(records, nameData);
268
  }
269
 
270
- // ── Helpers ──
 
 
271
 
272
- private static long ParseMftDataSize(ReadOnlySpan<byte> record)
 
 
 
 
273
  {
274
- if (record.Length < 48 || record[0] != 'F' || record[1] != 'I' || record[2] != 'L' || record[3] != 'E')
275
- return -1;
276
-
277
- if (!ApplyFixup(record, record.Length))
278
- return -1;
279
 
280
- ushort firstAttr = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x14, 2));
281
- int aoff = firstAttr;
282
-
283
- while (aoff + 0x40 <= record.Length)
284
- {
285
- uint atype = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff, 4));
286
- if (atype == 0xFFFFFFFF) break;
287
 
288
- uint alen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(aoff + 4, 4));
289
- if (alen == 0 || aoff + alen > record.Length) break;
290
 
291
- // 0x80 = $DATA, non-resident flag = 1
292
- if (atype == 0x80 && record[aoff + 8] == 1)
293
- {
294
- // Non-resident header: DataSize (RealSize) is at offset 0x30 from attr start
295
- long dataSize = BinaryPrimitives.ReadInt64LittleEndian(record.Slice(aoff + 0x30, 8));
296
- return dataSize;
297
- }
298
 
299
- aoff += (int)alen;
300
- }
301
 
302
- return -1;
 
303
  }
304
 
305
- private static bool ApplyFixup(ReadOnlySpan<byte> record, int recordSize)
 
 
 
 
 
306
  {
307
- if (record.Length < 48 || record[0] != 'F' || record[1] != 'I' || record[2] != 'L' || record[3] != 'E')
 
308
  return false;
309
 
310
- ushort fixupOff = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(4, 2));
311
- ushort fixupCnt = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(6, 2));
312
 
313
  if (fixupCnt < 2 || fixupOff + fixupCnt * 2 > recordSize)
314
  return false;
315
 
316
- byte check0 = record[fixupOff];
317
- byte check1 = record[fixupOff + 1];
318
 
319
  for (int i = 1; i < fixupCnt; i++)
320
  {
321
  int end = i * 512 - 2;
322
  if (end + 1 >= recordSize) break;
323
- if (record[end] != check0 || record[end + 1] != check1)
 
324
  return false;
 
 
 
 
325
  }
326
 
327
  return true;
328
  }
329
 
330
- private static void ParseFileRecord(ReadOnlySpan<byte> record, ulong mftIndex,
331
- List<CompactRecord> records, List<char> nameData)
 
 
 
 
 
 
 
332
  {
333
  ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x16, 2));
334
  if ((flags & 0x01) == 0) return; // Record not in use
@@ -365,7 +337,7 @@ public class MftReader : IDisposable
365
 
366
  if (vs + 66 + nlen * 2 <= record.Length)
367
  {
368
- if (ns == 2) // DOS 8.3 name, skip
369
  {
370
  aoff += (int)alen;
371
  continue;
@@ -373,9 +345,9 @@ public class MftReader : IDisposable
373
 
374
  byte priority = ns switch
375
  {
376
- 1 => 0, // POSIX
377
- 3 => 1, // Win32 & DOS
378
- 0 => 2, // Win32
379
  _ => 3,
380
  };
381
 
 
9
 
10
  public class MftReader : IDisposable
11
  {
12
+ // Volume handle β€” used for boot sector read and FSCTL fallback
13
  private readonly IntPtr _volumeHandle;
14
  private readonly NtfsDrive _drive;
15
  private bool _disposed;
16
 
17
+ private const int FallbackBuf = 4 * 1024 * 1024;
18
+ private const int DirectBuf = 4 * 1024 * 1024;
19
+
20
  public NtfsDrive Drive => _drive;
21
 
22
  public MftReader(NtfsDrive drive)
23
  {
24
  _drive = drive;
 
 
 
 
 
25
  _volumeHandle = Win32Api.CreateFileW(
26
  drive.DevicePath,
27
  Win32Api.GENERIC_READ,
 
36
  int err = Marshal.GetLastWin32Error();
37
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)}");
38
  }
 
 
39
  }
40
 
41
+ // ------------------------------------------------------------------
42
+ // Primary: direct $MFT file read (falls back to FSCTL if fails)
43
+ // ------------------------------------------------------------------
44
+
45
  /// <summary>
46
+ /// Read $MFT file directly for maximum speed. Returns null if unavailable.
47
+ /// Matches Rust scan_direct() exactly.
48
  /// </summary>
49
+ public ScanResult? ScanDirect()
50
  {
51
+ int? recordSize = ReadMftRecordSize();
52
+ if (!recordSize.HasValue) return null;
 
 
 
 
 
 
 
 
53
 
54
+ string mftPath = $"{_drive.Root}$MFT";
 
 
 
 
 
 
 
 
 
 
55
 
56
+ // Open the $MFT file as a normal file (not the volume device)
57
+ IntPtr mftHandle = Win32Api.CreateFileW(
58
+ mftPath,
59
+ Win32Api.GENERIC_READ,
60
+ Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
61
+ IntPtr.Zero,
62
+ Win32Api.OPEN_EXISTING,
63
+ Win32Api.FILE_FLAG_BACKUP_SEMANTICS | Win32Api.FILE_FLAG_SEQUENTIAL_SCAN,
64
+ IntPtr.Zero);
 
65
 
66
+ if (mftHandle == new IntPtr(-1))
67
+ return null;
 
 
 
 
68
 
69
+ try
 
 
 
 
 
 
70
  {
71
+ var records = new List<CompactRecord>();
72
+ var nameData = new List<char>();
73
+ byte[] buffer = new byte[DirectBuf];
74
+ ulong mftIndex = 0;
75
+ int leftover = 0;
 
 
76
 
77
+ while (true)
78
+ {
79
+ bool ok = Win32Api.ReadFile(
80
+ mftHandle, buffer, (uint)(buffer.Length - leftover), out uint bytesRead, IntPtr.Zero);
81
+ if (!ok || bytesRead == 0)
82
+ break;
 
83
 
84
+ int total = leftover + (int)bytesRead;
85
+ int offset = 0;
86
 
87
+ while (offset + recordSize.Value <= total)
 
 
 
 
 
 
88
  {
89
+ bool applied = ApplyFixup(buffer, offset, recordSize.Value);
90
+ if (applied)
91
+ {
92
+ ParseFileRecord(buffer.AsSpan(offset, recordSize.Value), mftIndex, records, nameData);
93
+ }
94
+
95
+ mftIndex++;
96
+ offset += recordSize.Value;
97
  }
 
 
 
 
 
 
 
 
98
 
99
+ // Align offset down to record boundary
100
+ offset = total - (total % recordSize.Value);
101
 
102
+ leftover = total - offset;
103
+ if (leftover > 0)
104
+ Array.Copy(buffer, offset, buffer, 0, leftover);
 
 
 
 
 
105
  }
106
 
107
+ Logger.Log($"[{Drive.Letter}] Direct scan: {records.Count} records from {mftIndex} positions");
108
+ return new ScanResult(records, nameData);
109
+ }
110
+ finally
111
+ {
112
+ Win32Api.CloseHandle(mftHandle);
113
  }
 
 
 
114
  }
115
 
116
+ // ------------------------------------------------------------------
117
+ // Fallback: FSCTL_ENUM_USN_DATA (4 MB buffer)
118
+ // ------------------------------------------------------------------
119
+
120
  public ScanResult Scan()
121
  {
 
 
122
  var records = new List<CompactRecord>();
123
  var nameData = new List<char>();
124
+ byte[] buffer = new byte[FallbackBuf];
125
 
126
  var enumData = new MFT_ENUM_DATA_V0
127
  {
 
160
  }
161
  if (error == 122) // ERROR_INSUFFICIENT_BUFFER
162
  {
163
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: buffer too small, retrying");
164
  buffer = new byte[buffer.Length * 2];
165
  continue;
166
  }
167
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed: {Win32Error(error)}");
168
+ break;
 
169
  }
170
 
171
  if (bytesReturned <= 8)
172
  {
173
+ Logger.Log($"[{Drive.Letter}] FSCTL: {bytesReturned} bytes, stopping");
174
  break;
175
  }
176
 
 
184
  IntPtr recordPtr = IntPtr.Add(bufferPtr, offset);
185
  var record = Marshal.PtrToStructure<USN_RECORD_V2>(recordPtr);
186
 
187
+ int recLen = (int)record.RecordLength;
188
+ if (recLen == 0 || offset + recLen > (int)bytesReturned) break;
189
 
190
  int nameOffset = offset + (int)record.FileNameOffset;
191
  int nameLen = record.FileNameLength / 2;
 
206
  IsDir = (record.FileAttributes & 0x10) != 0
207
  });
208
 
209
+ offset += recLen;
210
  }
211
  }
212
  finally
 
220
  Marshal.FreeHGlobal(enumDataPtr);
221
  }
222
 
223
+ Logger.Log($"[{Drive.Letter}] FSCTL scan: {records.Count} records");
224
  return new ScanResult(records, nameData);
225
  }
226
 
227
+ // ------------------------------------------------------------------
228
+ // NTFS helpers
229
+ // ------------------------------------------------------------------
230
 
231
+ /// <summary>
232
+ /// Read MFT record size from NTFS boot sector via volume handle.
233
+ /// Matches Rust read_mft_record_size().
234
+ /// </summary>
235
+ private int? ReadMftRecordSize()
236
  {
237
+ // Seek to beginning of volume
238
+ Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
 
 
 
239
 
240
+ byte[] boot = new byte[512];
241
+ bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out uint br, IntPtr.Zero);
242
+ if (!ok || br < 512)
243
+ return null;
 
 
 
244
 
245
+ if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
246
+ return null;
247
 
248
+ ushort bytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(boot.AsSpan(0x0B, 2));
249
+ byte sectorsPerCluster = boot[0x0D];
250
+ int clusterSize = bytesPerSector * sectorsPerCluster;
 
 
 
 
251
 
252
+ sbyte raw = (sbyte)boot[0x40];
253
+ int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw);
254
 
255
+ Logger.Log($"[{Drive.Letter}] Boot: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}");
256
+ return recordSize;
257
  }
258
 
259
+ /// <summary>
260
+ /// Apply NTFS multi-sector fixup. Returns false if record is invalid.
261
+ /// CRITICAL: Restores the original bytes from the fixup array back to sector ends.
262
+ /// Matches Rust apply_fixup() exactly.
263
+ /// </summary>
264
+ private static bool ApplyFixup(byte[] record, int offset, int recordSize)
265
  {
266
+ if (recordSize < 48 || record[offset] != 'F' || record[offset + 1] != 'I'
267
+ || record[offset + 2] != 'L' || record[offset + 3] != 'E')
268
  return false;
269
 
270
+ ushort fixupOff = BinaryPrimitives.ReadUInt16LittleEndian(record.AsSpan(offset + 4, 2));
271
+ ushort fixupCnt = BinaryPrimitives.ReadUInt16LittleEndian(record.AsSpan(offset + 6, 2));
272
 
273
  if (fixupCnt < 2 || fixupOff + fixupCnt * 2 > recordSize)
274
  return false;
275
 
276
+ byte check0 = record[offset + fixupOff];
277
+ byte check1 = record[offset + fixupOff + 1];
278
 
279
  for (int i = 1; i < fixupCnt; i++)
280
  {
281
  int end = i * 512 - 2;
282
  if (end + 1 >= recordSize) break;
283
+
284
+ if (record[offset + end] != check0 || record[offset + end + 1] != check1)
285
  return false;
286
+
287
+ // RESTORE the real bytes from the fixup array
288
+ record[offset + end] = record[offset + fixupOff + i * 2];
289
+ record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
290
  }
291
 
292
  return true;
293
  }
294
 
295
+ /// <summary>
296
+ /// Parse one MFT FILE record, extracting name + parent into the arena.
297
+ /// Matches Rust parse_file_record() exactly.
298
+ /// </summary>
299
+ private static void ParseFileRecord(
300
+ ReadOnlySpan<byte> record,
301
+ ulong mftIndex,
302
+ List<CompactRecord> records,
303
+ List<char> nameData)
304
  {
305
  ushort flags = BinaryPrimitives.ReadUInt16LittleEndian(record.Slice(0x16, 2));
306
  if ((flags & 0x01) == 0) return; // Record not in use
 
337
 
338
  if (vs + 66 + nlen * 2 <= record.Length)
339
  {
340
+ if (ns == 2) // DOS 8.3 name β€” skip
341
  {
342
  aoff += (int)alen;
343
  continue;
 
345
 
346
  byte priority = ns switch
347
  {
348
+ 1 => 0, // Win32
349
+ 3 => 1, // Win32 + DOS
350
+ 0 => 2, // POSIX
351
  _ => 3,
352
  };
353