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

Upload FastSeekWpf/Core/MftReader.cs

Browse files
Files changed (1) hide show
  1. 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 uint DirectBuf = 1024 * 1024; // 1MB read buffer
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
- Win32Api.FILE_FLAG_NO_BUFFERING | Win32Api.FILE_FLAG_SEQUENTIAL_SCAN,
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, find MFT LCN, read raw records from volume device.
41
- /// Matches the Rust implementation exactly.
42
  /// </summary>
43
  public ScanResult ScanDirect()
44
  {
45
- // Read boot sector (512 bytes, must be sector-aligned for NO_BUFFERING)
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(new ReadOnlySpan<byte>(boot, 0x0B, 2));
59
  byte sectorsPerCluster = boot[0x0D];
60
  int clusterSize = bytesPerSector * sectorsPerCluster;
61
 
62
- // MFT start LCN at offset 0x30 (signed 64-bit)
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}, mftOffset={mftByteOffset}");
69
 
70
- // Seek to MFT start
71
  ok = Win32Api.SetFilePointerEx(_volumeHandle, mftByteOffset, out _, 0);
72
  if (!ok)
 
 
 
 
 
 
 
 
 
 
73
  {
74
- int err = Marshal.GetLastWin32Error();
75
- throw new IOException($"Failed to seek to MFT offset {mftByteOffset}: {Win32Error(err)}");
 
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
- recordSize = 1 << (-raw);
 
 
85
 
86
- Logger.Log($"MFT record size: {recordSize} bytes (raw={raw})");
 
 
 
87
 
88
  var records = new List<CompactRecord>();
89
  var nameData = new List<char>();
90
- byte[] buffer = new byte[DirectBuf];
91
  ulong mftIndex = 0;
92
- int leftover = 0;
93
- long totalBytesRead = 0;
 
94
 
95
- while (true)
96
  {
97
- ok = Win32Api.ReadFile(_volumeHandle, buffer, (uint)(buffer.Length - leftover), out uint bytesRead, IntPtr.Zero);
 
98
  if (!ok)
99
  {
100
  int err = Marshal.GetLastWin32Error();
101
- if (err == 38) // ERROR_HANDLE_EOF β€” reached end of MFT
102
  {
103
- Logger.Log("MFT read: reached EOF");
104
  break;
105
  }
106
- throw new IOException($"ReadFile failed at offset {mftByteOffset + totalBytesRead}: {Win32Error(err)}");
 
107
  }
108
  if (bytesRead == 0)
109
  {
110
- Logger.Log("MFT read: 0 bytes returned");
111
  break;
112
  }
113
 
114
- totalBytesRead += bytesRead;
115
- int total = leftover + (int)bytesRead;
116
  int offset = 0;
 
117
 
118
  while (offset + recordSize <= total)
119
  {
120
- var span = new ReadOnlySpan<byte>(buffer, offset, recordSize);
121
  if (ApplyFixup(span, recordSize))
122
  {
123
  ParseFileRecord(span, mftIndex, records, nameData);
 
 
 
 
 
124
  }
125
  mftIndex++;
126
  offset += recordSize;
127
- }
128
 
129
- leftover = total - offset;
130
- if (leftover > 0)
131
- Array.Copy(buffer, offset, buffer, 0, leftover);
 
 
 
 
132
  }
133
 
134
- Logger.Log($"MFT read complete: {records.Count} records from {mftIndex} record positions");
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]; // 4MB
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')