anshdadhich commited on
Commit
70e3a96
·
verified ·
1 Parent(s): d524699

Upload FastSeekWpf/Core/MftReader.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/MftReader.cs +183 -79
FastSeekWpf/Core/MftReader.cs CHANGED
@@ -3,6 +3,7 @@ using System.Buffers.Binary;
3
  using System.Collections.Generic;
4
  using System.IO;
5
  using System.Runtime.InteropServices;
 
6
  using FastSeekWpf.NativeInterop;
7
 
8
  namespace FastSeekWpf.Core;
@@ -11,6 +12,8 @@ public class MftReader : IDisposable
11
  {
12
  private readonly IntPtr _volumeHandle;
13
  private readonly NtfsDrive _drive;
 
 
14
  private bool _disposed;
15
 
16
  private const int FallbackBuf = 4 * 1024 * 1024;
@@ -35,13 +38,58 @@ public class MftReader : IDisposable
35
  int err = Marshal.GetLastWin32Error();
36
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
37
  }
 
 
 
 
 
 
 
 
38
  }
39
 
40
- public ScanResult? ScanDirect()
 
 
 
 
 
 
 
41
  {
42
- int? recordSize = ReadMftRecordSize();
43
- if (!recordSize.HasValue) return null;
 
 
 
 
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  string mftPath = $"{_drive.Root}$MFT";
46
  Logger.Log($"[{Drive.Letter}] Opening $MFT file: {mftPath}");
47
 
@@ -63,55 +111,8 @@ public class MftReader : IDisposable
63
 
64
  try
65
  {
66
- var records = new List<CompactRecord>();
67
- var nameData = new List<char>();
68
- byte[] buffer = new byte[DirectBuf];
69
- ulong mftIndex = 0;
70
- int leftover = 0;
71
- long totalRead = 0;
72
-
73
- while (true)
74
- {
75
- bool ok = Win32Api.ReadFile(
76
- mftHandle, buffer, leftover, (uint)(buffer.Length - leftover), out uint bytesRead, IntPtr.Zero);
77
- if (!ok)
78
- {
79
- int err = Marshal.GetLastWin32Error();
80
- Logger.Log($"[{Drive.Letter}] ReadFile failed at offset {totalRead}: {Win32Error(err)} (code={err})");
81
- break;
82
- }
83
- if (bytesRead == 0)
84
- {
85
- Logger.Log($"[{Drive.Letter}] ReadFile returned 0 bytes at offset {totalRead} — EOF");
86
- break;
87
- }
88
-
89
- totalRead += bytesRead;
90
- int total = leftover + (int)bytesRead;
91
- int offset = 0;
92
-
93
- while (offset + recordSize.Value <= total)
94
- {
95
- bool applied = ApplyFixup(buffer, offset, recordSize.Value);
96
- if (applied)
97
- {
98
- ParseFileRecord(buffer.AsSpan(offset, recordSize.Value), mftIndex, records, nameData);
99
- }
100
- mftIndex++;
101
- offset += recordSize.Value;
102
- }
103
-
104
- offset = total - (total % recordSize.Value);
105
- leftover = total - offset;
106
- if (leftover > 0)
107
- Array.Copy(buffer, offset, buffer, 0, leftover);
108
-
109
- if (mftIndex % 50000 == 0)
110
- Logger.Log($"[{Drive.Letter}] Progress: {mftIndex:N0} records parsed, {records.Count:N0} valid");
111
- }
112
-
113
- Logger.Log($"[{Drive.Letter}] Direct scan: {records.Count} records from {mftIndex} positions, {totalRead:N0} bytes read");
114
- return new ScanResult(records, nameData);
115
  }
116
  finally
117
  {
@@ -119,38 +120,107 @@ public class MftReader : IDisposable
119
  }
120
  }
121
 
122
- public ScanResult Scan()
 
 
 
 
123
  {
124
- // Diagnostic: test FSCTL_QUERY_USN_JOURNAL first
125
- Logger.Log($"[{Drive.Letter}] Diagnostic: testing FSCTL_QUERY_USN_JOURNAL...");
126
- IntPtr journalBuf = Marshal.AllocHGlobal(Marshal.SizeOf<USN_JOURNAL_DATA_V0>());
 
 
 
 
 
 
 
127
  try
128
  {
129
- bool ok = Win32Api.DeviceIoControl(
130
- _volumeHandle, Win32Api.FSCTL_QUERY_USN_JOURNAL,
131
- IntPtr.Zero, 0,
132
- journalBuf, (uint)Marshal.SizeOf<USN_JOURNAL_DATA_V0>(),
133
- out uint bytesReturned, IntPtr.Zero);
134
- if (ok)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  {
136
- var journalData = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(journalBuf);
137
- Logger.Log($"[{Drive.Letter}] USN journal OK: ID={journalData.UsnJournalID}, NextUsn={journalData.NextUsn}, Active=YES");
 
 
 
 
 
 
138
  }
139
- else
140
  {
141
- int err = Marshal.GetLastWin32Error();
142
- Logger.Log($"[{Drive.Letter}] FSCTL_QUERY_USN_JOURNAL failed: {Win32Error(err)} (code={err})");
143
  }
144
- }
145
- finally
146
- {
147
- Marshal.FreeHGlobal(journalBuf);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
 
 
 
 
 
 
 
 
 
150
  Logger.Log($"[{Drive.Letter}] Starting FSCTL_ENUM_USN_DATA fallback...");
151
  Logger.Log($"[{Drive.Letter}] MFT_ENUM_DATA_V0 size = {Marshal.SizeOf<MFT_ENUM_DATA_V0>()} bytes");
152
  Logger.Log($"[{Drive.Letter}] USN_RECORD_V2 size = {Marshal.SizeOf<USN_RECORD_V2>()} bytes");
153
 
 
 
 
154
  var records = new List<CompactRecord>();
155
  var nameData = new List<char>();
156
  byte[] buffer = new byte[FallbackBuf];
@@ -196,10 +266,6 @@ public class MftReader : IDisposable
196
  buffer = new byte[buffer.Length * 2];
197
  continue;
198
  }
199
- if (error == 1) // ERROR_INVALID_FUNCTION
200
- {
201
- Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: ERROR_INVALID_FUNCTION — struct/IOCTL mismatch or journal not active");
202
- }
203
  Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)} (code={error})");
204
  break;
205
  }
@@ -260,7 +326,43 @@ public class MftReader : IDisposable
260
  return new ScanResult(records, nameData);
261
  }
262
 
263
- private int? ReadMftRecordSize()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  {
265
  Logger.Log($"[{Drive.Letter}] Reading boot sector from volume...");
266
  Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
@@ -276,7 +378,7 @@ public class MftReader : IDisposable
276
 
277
  if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
278
  {
279
- Logger.Log($"[{Drive.Letter}] Not NTFS (missing signature at offset 3): bytes={boot[3]},{boot[4]},{boot[5]},{boot[6]}");
280
  return null;
281
  }
282
 
@@ -284,11 +386,13 @@ public class MftReader : IDisposable
284
  byte sectorsPerCluster = boot[0x0D];
285
  int clusterSize = bytesPerSector * sectorsPerCluster;
286
 
 
287
  sbyte raw = (sbyte)boot[0x40];
288
  int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw);
 
289
 
290
- Logger.Log($"[{Drive.Letter}] Boot OK: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}");
291
- return recordSize;
292
  }
293
 
294
  private static bool ApplyFixup(byte[] record, int offset, int recordSize)
 
3
  using System.Collections.Generic;
4
  using System.IO;
5
  using System.Runtime.InteropServices;
6
+ using System.Security.Principal;
7
  using FastSeekWpf.NativeInterop;
8
 
9
  namespace FastSeekWpf.Core;
 
12
  {
13
  private readonly IntPtr _volumeHandle;
14
  private readonly NtfsDrive _drive;
15
+ private readonly int _recordSize;
16
+ private readonly long _mftByteOffset;
17
  private bool _disposed;
18
 
19
  private const int FallbackBuf = 4 * 1024 * 1024;
 
38
  int err = Marshal.GetLastWin32Error();
39
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)} (code={err})");
40
  }
41
+
42
+ // Read boot sector once and cache record size + MFT offset
43
+ var bootInfo = ReadBootSector();
44
+ if (!bootInfo.HasValue)
45
+ throw new IOException("Failed to read NTFS boot sector");
46
+
47
+ _recordSize = bootInfo.Value.recordSize;
48
+ _mftByteOffset = bootInfo.Value.mftOffset;
49
  }
50
 
51
+ // ------------------------------------------------------------------
52
+ // Scan methods — try in order: $MFT file → volume raw → FSCTL
53
+ // ------------------------------------------------------------------
54
+
55
+ /// <summary>
56
+ /// Try all scan methods and return the first that works.
57
+ /// </summary>
58
+ public ScanResult ScanAny()
59
  {
60
+ // 1. Try $MFT file (fastest, matches Rust)
61
+ var direct = ScanDirect();
62
+ if (direct != null && direct.Records.Count > 0)
63
+ {
64
+ Logger.Log($"[{Drive.Letter}] Using direct $MFT file scan: {direct.Records.Count} records");
65
+ return direct;
66
+ }
67
 
68
+ // 2. Try reading raw from volume handle
69
+ var vol = ScanDirectVolume();
70
+ if (vol != null && vol.Records.Count > 0)
71
+ {
72
+ Logger.Log($"[{Drive.Letter}] Using volume raw scan: {vol.Records.Count} records");
73
+ return vol;
74
+ }
75
+
76
+ // 3. FSCTL fallback (slowest but most compatible)
77
+ var fallback = Scan();
78
+ if (fallback.Records.Count > 0)
79
+ {
80
+ Logger.Log($"[{Drive.Letter}] Using FSCTL fallback scan: {fallback.Records.Count} records");
81
+ return fallback;
82
+ }
83
+
84
+ Logger.Log($"[{Drive.Letter}] ALL scan methods returned 0 records");
85
+ return fallback;
86
+ }
87
+
88
+ /// <summary>
89
+ /// Direct $MFT file read. Matches Rust scan_direct().
90
+ /// </summary>
91
+ public ScanResult? ScanDirect()
92
+ {
93
  string mftPath = $"{_drive.Root}$MFT";
94
  Logger.Log($"[{Drive.Letter}] Opening $MFT file: {mftPath}");
95
 
 
111
 
112
  try
113
  {
114
+ var result = ScanFromHandle(mftHandle, "direct-file");
115
+ return result;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  }
117
  finally
118
  {
 
120
  }
121
  }
122
 
123
+ /// <summary>
124
+ /// Read MFT records directly from the volume handle at the boot-sector offset.
125
+ /// Fallback when $MFT file is blocked.
126
+ /// </summary>
127
+ public ScanResult? ScanDirectVolume()
128
  {
129
+ Logger.Log($"[{Drive.Letter}] Attempting volume raw read at offset {_mftByteOffset}...");
130
+
131
+ bool ok = Win32Api.SetFilePointerEx(_volumeHandle, _mftByteOffset, out _, 0);
132
+ if (!ok)
133
+ {
134
+ int err = Marshal.GetLastWin32Error();
135
+ Logger.Log($"[{Drive.Letter}] Seek to MFT offset failed: {Win32Error(err)} (code={err})");
136
+ return null;
137
+ }
138
+
139
  try
140
  {
141
+ var result = ScanFromHandle(_volumeHandle, "direct-volume");
142
+ return result;
143
+ }
144
+ catch (Exception ex)
145
+ {
146
+ Logger.Log($"[{Drive.Letter}] Volume raw read failed: {ex.Message}");
147
+ return null;
148
+ }
149
+ }
150
+
151
+ /// <summary>
152
+ /// Common scan loop used by both direct-file and direct-volume.
153
+ /// </summary>
154
+ private ScanResult ScanFromHandle(IntPtr handle, string methodName)
155
+ {
156
+ var records = new List<CompactRecord>();
157
+ var nameData = new List<char>();
158
+ byte[] buffer = new byte[DirectBuf];
159
+ ulong mftIndex = 0;
160
+ int leftover = 0;
161
+ long totalRead = 0;
162
+
163
+ while (true)
164
+ {
165
+ bool ok = Win32Api.ReadFile(
166
+ handle, buffer, leftover, (uint)(buffer.Length - leftover), out uint bytesRead, IntPtr.Zero);
167
+ if (!ok)
168
  {
169
+ int err = Marshal.GetLastWin32Error();
170
+ if (err == 38) // ERROR_HANDLE_EOF
171
+ {
172
+ Logger.Log($"[{Drive.Letter}] {methodName}: EOF at offset {totalRead}");
173
+ break;
174
+ }
175
+ Logger.Log($"[{Drive.Letter}] {methodName}: ReadFile failed at offset {totalRead}: {Win32Error(err)} (code={err})");
176
+ break;
177
  }
178
+ if (bytesRead == 0)
179
  {
180
+ Logger.Log($"[{Drive.Letter}] {methodName}: ReadFile returned 0 bytes at offset {totalRead} — EOF");
181
+ break;
182
  }
183
+
184
+ totalRead += bytesRead;
185
+ int total = leftover + (int)bytesRead;
186
+ int offset = 0;
187
+
188
+ while (offset + _recordSize <= total)
189
+ {
190
+ bool applied = ApplyFixup(buffer, offset, _recordSize);
191
+ if (applied)
192
+ {
193
+ ParseFileRecord(buffer.AsSpan(offset, _recordSize), mftIndex, records, nameData);
194
+ }
195
+ mftIndex++;
196
+ offset += _recordSize;
197
+ }
198
+
199
+ offset = total - (total % _recordSize);
200
+ leftover = total - offset;
201
+ if (leftover > 0)
202
+ Array.Copy(buffer, offset, buffer, 0, leftover);
203
+
204
+ if (mftIndex % 50000 == 0)
205
+ Logger.Log($"[{Drive.Letter}] {methodName}: {mftIndex:N0} records parsed, {records.Count:N0} valid");
206
  }
207
 
208
+ Logger.Log($"[{Drive.Letter}] {methodName}: {records.Count} records from {mftIndex} positions, {totalRead:N0} bytes read");
209
+ return new ScanResult(records, nameData);
210
+ }
211
+
212
+ /// <summary>
213
+ /// FSCTL_ENUM_USN_DATA fallback.
214
+ /// </summary>
215
+ public ScanResult Scan()
216
+ {
217
  Logger.Log($"[{Drive.Letter}] Starting FSCTL_ENUM_USN_DATA fallback...");
218
  Logger.Log($"[{Drive.Letter}] MFT_ENUM_DATA_V0 size = {Marshal.SizeOf<MFT_ENUM_DATA_V0>()} bytes");
219
  Logger.Log($"[{Drive.Letter}] USN_RECORD_V2 size = {Marshal.SizeOf<USN_RECORD_V2>()} bytes");
220
 
221
+ // Diagnostic: test FSCTL_QUERY_USN_JOURNAL
222
+ TestUsnJournal();
223
+
224
  var records = new List<CompactRecord>();
225
  var nameData = new List<char>();
226
  byte[] buffer = new byte[FallbackBuf];
 
266
  buffer = new byte[buffer.Length * 2];
267
  continue;
268
  }
 
 
 
 
269
  Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA failed (iter {iterations}): {Win32Error(error)} (code={error})");
270
  break;
271
  }
 
326
  return new ScanResult(records, nameData);
327
  }
328
 
329
+ // ------------------------------------------------------------------
330
+ // Diagnostics
331
+ // ------------------------------------------------------------------
332
+
333
+ private void TestUsnJournal()
334
+ {
335
+ Logger.Log($"[{Drive.Letter}] Diagnostic: testing FSCTL_QUERY_USN_JOURNAL...");
336
+ IntPtr journalBuf = Marshal.AllocHGlobal(Marshal.SizeOf<USN_JOURNAL_DATA_V0>());
337
+ try
338
+ {
339
+ bool ok = Win32Api.DeviceIoControl(
340
+ _volumeHandle, Win32Api.FSCTL_QUERY_USN_JOURNAL,
341
+ IntPtr.Zero, 0,
342
+ journalBuf, (uint)Marshal.SizeOf<USN_JOURNAL_DATA_V0>(),
343
+ out uint bytesReturned, IntPtr.Zero);
344
+ if (ok)
345
+ {
346
+ var journalData = Marshal.PtrToStructure<USN_JOURNAL_DATA_V0>(journalBuf);
347
+ Logger.Log($"[{Drive.Letter}] USN journal OK: ID={journalData.UsnJournalID}, NextUsn={journalData.NextUsn}");
348
+ }
349
+ else
350
+ {
351
+ int err = Marshal.GetLastWin32Error();
352
+ Logger.Log($"[{Drive.Letter}] FSCTL_QUERY_USN_JOURNAL failed: {Win32Error(err)} (code={err})");
353
+ }
354
+ }
355
+ finally
356
+ {
357
+ Marshal.FreeHGlobal(journalBuf);
358
+ }
359
+ }
360
+
361
+ // ------------------------------------------------------------------
362
+ // NTFS helpers
363
+ // ------------------------------------------------------------------
364
+
365
+ private (int recordSize, long mftOffset)? ReadBootSector()
366
  {
367
  Logger.Log($"[{Drive.Letter}] Reading boot sector from volume...");
368
  Win32Api.SetFilePointerEx(_volumeHandle, 0, out _, 0);
 
378
 
379
  if (boot[3] != 'N' || boot[4] != 'T' || boot[5] != 'F' || boot[6] != 'S')
380
  {
381
+ Logger.Log($"[{Drive.Letter}] Not NTFS (missing signature at offset 3)");
382
  return null;
383
  }
384
 
 
386
  byte sectorsPerCluster = boot[0x0D];
387
  int clusterSize = bytesPerSector * sectorsPerCluster;
388
 
389
+ long mftStartLcn = BinaryPrimitives.ReadInt64LittleEndian(boot.AsSpan(0x30, 8));
390
  sbyte raw = (sbyte)boot[0x40];
391
  int recordSize = raw > 0 ? raw * clusterSize : 1 << (-raw);
392
+ long mftOffset = mftStartLcn * clusterSize;
393
 
394
+ Logger.Log($"[{Drive.Letter}] Boot OK: bps={bytesPerSector}, spc={sectorsPerCluster}, clusterSize={clusterSize}, recordSize={recordSize}, mftLcn={mftStartLcn}, mftOffset={mftOffset}");
395
+ return (recordSize, mftOffset);
396
  }
397
 
398
  private static bool ApplyFixup(byte[] record, int offset, int recordSize)