anshdadhich commited on
Commit
61c1a28
Β·
verified Β·
1 Parent(s): c0302db

Upload FastSeekWpf/Core/MftReader.cs

Browse files
Files changed (1) hide show
  1. FastSeekWpf/Core/MftReader.cs +52 -59
FastSeekWpf/Core/MftReader.cs CHANGED
@@ -13,24 +13,23 @@ public class MftReader : IDisposable
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))
@@ -38,20 +37,27 @@ public class MftReader : IDisposable
38
  int err = Marshal.GetLastWin32Error();
39
  throw new IOException($"Failed to open volume {drive.DevicePath}: {Win32Error(err)}");
40
  }
 
 
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
 
@@ -67,103 +73,95 @@ public class MftReader : IDisposable
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];
@@ -200,12 +198,12 @@ public class MftReader : IDisposable
200
  int error = Marshal.GetLastWin32Error();
201
  if ((uint)error == 0x26) // ERROR_HANDLE_EOF
202
  {
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
  }
@@ -216,7 +214,7 @@ public class MftReader : IDisposable
216
 
217
  if (bytesReturned <= 8)
218
  {
219
- Logger.Log($"FSCTL_ENUM_USN_DATA: {bytesReturned} bytes returned, stopping");
220
  break;
221
  }
222
 
@@ -265,22 +263,17 @@ public class MftReader : IDisposable
265
  Marshal.FreeHGlobal(enumDataPtr);
266
  }
267
 
268
- Logger.Log($"FSCTL scan complete: {records.Count} records");
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
 
 
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,
29
  Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
30
  IntPtr.Zero,
31
  Win32Api.OPEN_EXISTING,
32
+ Win32Api.FILE_FLAG_BACKUP_SEMANTICS,
33
  IntPtr.Zero);
34
 
35
  if (_volumeHandle == new IntPtr(-1))
 
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
 
 
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];
 
198
  int error = Marshal.GetLastWin32Error();
199
  if ((uint)error == 0x26) // ERROR_HANDLE_EOF
200
  {
201
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
202
  break;
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
  }
 
214
 
215
  if (bytesReturned <= 8)
216
  {
217
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: {bytesReturned} bytes returned, stopping");
218
  break;
219
  }
220
 
 
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