Fix Rust parity: CompleteIndex, DriveDiscovery using, NtfsDrive.DevicePath, UsnWatcher.RunShared

#1
by anshdadhich - opened
FastSeekWpf/App.xaml.cs CHANGED
@@ -115,9 +115,12 @@ public partial class App : Application
115
  PrintHeader("FastSeek CLI");
116
  Console.WriteLine();
117
 
 
118
  bool elevated = Elevation.IsElevated();
119
  if (elevated)
 
120
  PrintLine(ConsoleColor.Green, "Elevation", "Administrator βœ“");
 
121
  else
122
  {
123
  PrintLine(ConsoleColor.Red, "Elevation", $"{Elevation.StatusText()} βœ—");
@@ -128,6 +131,7 @@ public partial class App : Application
128
  Console.WriteLine();
129
  }
130
 
 
131
  var drives = DriveDiscovery.GetNtfsDrives();
132
  PrintLine(ConsoleColor.Cyan, "Drives", $"{drives.Count} NTFS found");
133
  foreach (var d in drives) PrintDetail($" {d.Letter}: {d.Root}");
@@ -143,8 +147,11 @@ public partial class App : Application
143
  }
144
 
145
  if (!elevated)
 
146
  PrintInfo("Attempting scan anyway (will likely fail without elevation)...");
 
147
 
 
148
  PrintLine(ConsoleColor.Cyan, "Scan", "Reading MFT...");
149
  var index = new IndexStore();
150
  long totalRecords = 0;
@@ -176,12 +183,13 @@ public partial class App : Application
176
  }
177
  }
178
 
179
- index.Finalize();
180
  sw.Stop();
181
 
182
  PrintLine(ConsoleColor.White, "Index", $"{index.Count:N0} entries | {sw.ElapsedMilliseconds}ms | {totalRecords / Math.Max(sw.ElapsedMilliseconds, 1):N0} rec/ms");
183
  Console.WriteLine();
184
 
 
185
  if (index.Count > 0)
186
  {
187
  var cacheSw = Stopwatch.StartNew();
@@ -192,22 +200,25 @@ public partial class App : Application
192
  PrintLine(ConsoleColor.Cyan, "Cache", $"saved {FormatBytes(cacheSize)} in {cacheSw.ElapsedMilliseconds}ms");
193
  }
194
  else
 
195
  PrintLine(ConsoleColor.DarkGray, "Cache", "skipped (empty index)");
 
196
  Console.WriteLine();
197
 
 
198
  if (index.Count > 0)
199
  {
200
  PrintLine(ConsoleColor.Cyan, "Benchmark", "1,000 random queries...");
201
  var benchSw = Stopwatch.StartNew();
202
  var rnd = new Random(42);
 
203
  var exclusions = CacheManager.LoadExclusions();
204
  long benchResults = 0;
205
 
206
  for (int i = 0; i < 1000; i++)
207
  {
208
- if (index.Entries.Count == 0) break;
209
- var entry = index.Entries[rnd.Next(index.Entries.Count)];
210
- var name = index.Name(entry);
211
  if (name.Length < 2) continue;
212
  int start = rnd.Next(name.Length - 1);
213
  int len = rnd.Next(1, Math.Min(name.Length - start, 6));
@@ -221,7 +232,9 @@ public partial class App : Application
221
  PrintLine(ConsoleColor.White, "Result", $"{benchSw.ElapsedMilliseconds}ms total | {qps:N0} qps | {benchResults / 1000.0:F1} avg results");
222
  }
223
  else
 
224
  PrintLine(ConsoleColor.DarkGray, "Benchmark", "skipped (empty index)");
 
225
  Console.WriteLine();
226
  PrintDivider();
227
  Console.WriteLine();
@@ -248,6 +261,7 @@ public partial class App : Application
248
  Console.WriteLine();
249
  }
250
 
 
251
  if (index.Count > 0)
252
  {
253
  PrintHeader("Interactive Search (type 'exit' to quit)");
 
115
  PrintHeader("FastSeek CLI");
116
  Console.WriteLine();
117
 
118
+ // ── Elevation check ──
119
  bool elevated = Elevation.IsElevated();
120
  if (elevated)
121
+ {
122
  PrintLine(ConsoleColor.Green, "Elevation", "Administrator βœ“");
123
+ }
124
  else
125
  {
126
  PrintLine(ConsoleColor.Red, "Elevation", $"{Elevation.StatusText()} βœ—");
 
131
  Console.WriteLine();
132
  }
133
 
134
+ // 1. Drives
135
  var drives = DriveDiscovery.GetNtfsDrives();
136
  PrintLine(ConsoleColor.Cyan, "Drives", $"{drives.Count} NTFS found");
137
  foreach (var d in drives) PrintDetail($" {d.Letter}: {d.Root}");
 
147
  }
148
 
149
  if (!elevated)
150
+ {
151
  PrintInfo("Attempting scan anyway (will likely fail without elevation)...");
152
+ }
153
 
154
+ // 2. Scan
155
  PrintLine(ConsoleColor.Cyan, "Scan", "Reading MFT...");
156
  var index = new IndexStore();
157
  long totalRecords = 0;
 
183
  }
184
  }
185
 
186
+ index.CompleteIndex();
187
  sw.Stop();
188
 
189
  PrintLine(ConsoleColor.White, "Index", $"{index.Count:N0} entries | {sw.ElapsedMilliseconds}ms | {totalRecords / Math.Max(sw.ElapsedMilliseconds, 1):N0} rec/ms");
190
  Console.WriteLine();
191
 
192
+ // 3. Cache
193
  if (index.Count > 0)
194
  {
195
  var cacheSw = Stopwatch.StartNew();
 
200
  PrintLine(ConsoleColor.Cyan, "Cache", $"saved {FormatBytes(cacheSize)} in {cacheSw.ElapsedMilliseconds}ms");
201
  }
202
  else
203
+ {
204
  PrintLine(ConsoleColor.DarkGray, "Cache", "skipped (empty index)");
205
+ }
206
  Console.WriteLine();
207
 
208
+ // 4. Benchmark
209
  if (index.Count > 0)
210
  {
211
  PrintLine(ConsoleColor.Cyan, "Benchmark", "1,000 random queries...");
212
  var benchSw = Stopwatch.StartNew();
213
  var rnd = new Random(42);
214
+ var names = index.NameCache;
215
  var exclusions = CacheManager.LoadExclusions();
216
  long benchResults = 0;
217
 
218
  for (int i = 0; i < 1000; i++)
219
  {
220
+ if (names.Count == 0) break;
221
+ var name = names[rnd.Next(names.Count)];
 
222
  if (name.Length < 2) continue;
223
  int start = rnd.Next(name.Length - 1);
224
  int len = rnd.Next(1, Math.Min(name.Length - start, 6));
 
232
  PrintLine(ConsoleColor.White, "Result", $"{benchSw.ElapsedMilliseconds}ms total | {qps:N0} qps | {benchResults / 1000.0:F1} avg results");
233
  }
234
  else
235
+ {
236
  PrintLine(ConsoleColor.DarkGray, "Benchmark", "skipped (empty index)");
237
+ }
238
  Console.WriteLine();
239
  PrintDivider();
240
  Console.WriteLine();
 
261
  Console.WriteLine();
262
  }
263
 
264
+ // 5. Interactive search
265
  if (index.Count > 0)
266
  {
267
  PrintHeader("Interactive Search (type 'exit' to quit)");
FastSeekWpf/Core/CacheManager.cs CHANGED
@@ -48,7 +48,14 @@ public static class CacheManager
48
  byte[] compressed = File.ReadAllBytes(CachePath);
49
  byte[] bytes = LZ4Pickler.Unpickle(compressed);
50
  string json = Encoding.UTF8.GetString(bytes);
51
- return JsonSerializer.Deserialize<CacheData>(json, JsonOptions);
 
 
 
 
 
 
 
52
  }
53
  catch
54
  {
 
48
  byte[] compressed = File.ReadAllBytes(CachePath);
49
  byte[] bytes = LZ4Pickler.Unpickle(compressed);
50
  string json = Encoding.UTF8.GetString(bytes);
51
+ var cache = JsonSerializer.Deserialize<CacheData>(json, JsonOptions);
52
+ // Back-compat: single DriveRoot β†’ DriveRoots list
53
+ if (cache != null && cache.DriveRoots == null || cache!.DriveRoots.Count == 0)
54
+ {
55
+ // This is the old format β€” can't use it, need full rescan
56
+ return null;
57
+ }
58
+ return cache;
59
  }
60
  catch
61
  {
FastSeekWpf/Core/IndexStore.cs CHANGED
@@ -12,17 +12,17 @@ public class CachedEntry
12
  public ulong ParentRef { get; set; }
13
  public string Name { get; set; } = string.Empty;
14
  public FileKind Kind { get; set; }
 
15
  }
16
 
17
  [Serializable]
18
  public class CacheData
19
  {
20
  public List<CachedEntry> Entries { get; set; } = new();
21
- public string DriveRoot { get; set; } = string.Empty;
22
  public List<JournalCheckpoint> Checkpoints { get; set; } = new();
23
  }
24
 
25
- // Compact in-memory entry β€” matches Rust IndexEntry exactly
26
  public struct IndexEntry
27
  {
28
  public ulong FileRef;
@@ -32,35 +32,29 @@ public struct IndexEntry
32
  public ushort NameLen;
33
  public ushort NameLowerLen;
34
  public byte Flags; // bit 0 = is_dir
 
35
 
36
  public readonly bool IsDir => (Flags & 1) != 0;
37
  public readonly FileKind Kind => IsDir ? FileKind.Directory : FileKind.File;
38
  }
39
 
40
- // Main index store β€” matches Rust IndexStore exactly (single drive_root)
41
  public class IndexStore
42
  {
43
  public List<IndexEntry> Entries = new();
44
- public List<byte> NameArena = new(); // UTF-8 name bytes
45
- public List<byte> NameLowerArena = new(); // UTF-8 lowercase name bytes
46
- public List<(ulong fileRef, int idx)> RefLookup = new(); // sorted by file_ref
47
- public string DriveRoot = string.Empty;
 
 
48
  public List<JournalCheckpoint> Checkpoints = new();
49
 
50
  public int Count => Entries.Count;
51
 
52
- // Arena accessors β€” matches Rust name() and name_lower()
53
- public string Name(IndexEntry e)
54
- {
55
- return Encoding.UTF8.GetString(NameArena.ToArray(), (int)e.NameOff, e.NameLen);
56
- }
57
-
58
- public string NameLower(IndexEntry e)
59
- {
60
- return Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)e.NameLowerOff, e.NameLowerLen);
61
- }
62
 
63
- // Ref lookup (binary search) β€” matches Rust lookup_idx()
64
  public uint? LookupIdx(ulong fileRef)
65
  {
66
  int lo = 0, hi = RefLookup.Count - 1;
@@ -84,14 +78,26 @@ public class IndexStore
84
  RefLookup.Sort((a, b) => a.fileRef.CompareTo(b.fileRef));
85
  }
86
 
87
- // Populate from MFT scan β€” matches Rust populate_from_scan()
88
  public void PopulateFromScan(ScanResult scan, string driveRoot)
89
  {
90
- this.DriveRoot = driveRoot;
 
 
 
 
 
 
 
 
 
 
 
91
  int count = scan.Records.Count;
92
- Entries.Capacity = count;
93
- NameArena.Capacity = count * 30;
94
- NameLowerArena.Capacity = count * 30;
 
 
95
 
96
  foreach (var r in scan.Records)
97
  {
@@ -103,13 +109,16 @@ public class IndexStore
103
  string name = new string(nameChars);
104
  string nameLower = name.ToLowerInvariant();
105
 
 
 
 
106
  uint nOff = (uint)NameArena.Count;
107
- ushort nLen = (ushort)name.Length;
108
- NameArena.AddRange(Encoding.UTF8.GetBytes(name));
109
 
110
  uint nlOff = (uint)NameLowerArena.Count;
111
- ushort nlLen = (ushort)nameLower.Length;
112
- NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower));
113
 
114
  Entries.Add(new IndexEntry
115
  {
@@ -119,77 +128,90 @@ public class IndexStore
119
  NameLowerOff = nlOff,
120
  NameLen = nLen,
121
  NameLowerLen = nlLen,
122
- Flags = r.IsDir ? (byte)1 : (byte)0
 
123
  });
 
 
 
124
  }
125
  }
126
 
127
- // Sort entries by lowercase name and rebuild lookup β€” matches Rust finalize()
128
- public void Finalize()
 
 
 
129
  {
130
- // Rust uses sort_unstable_by with store_ptr for name_lower comparison.
131
- // We use a stable sort with proper key extraction.
132
  var indices = Enumerable.Range(0, Entries.Count).ToArray();
133
- Array.Sort(indices, (a, b) =>
134
- {
135
- var ea = Entries[a];
136
- var eb = Entries[b];
137
- return string.CompareOrdinal(
138
- Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)ea.NameLowerOff, ea.NameLowerLen),
139
- Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)eb.NameLowerOff, eb.NameLowerLen));
140
- });
141
 
142
- var sorted = new List<IndexEntry>(Entries.Count);
143
  foreach (var i in indices)
144
- sorted.Add(Entries[i]);
145
- Entries = sorted;
 
 
 
146
 
 
 
 
147
  RebuildRefLookup();
 
148
  NameArena.TrimExcess();
149
  NameLowerArena.TrimExcess();
150
  }
151
 
152
- // Cache serialization β€” matches Rust to_cache()
153
  public CacheData ToCache()
154
  {
155
  return new CacheData
156
  {
157
- Entries = Entries.Select(e => new CachedEntry
158
  {
159
  FileRef = e.FileRef,
160
  ParentRef = e.ParentRef,
161
- Name = Name(e),
162
- Kind = e.Kind
 
163
  }).ToList(),
164
- DriveRoot = DriveRoot,
165
  Checkpoints = new List<JournalCheckpoint>(Checkpoints)
166
  };
167
  }
168
 
169
- // Cache deserialization β€” matches Rust from_cache()
170
  public static IndexStore FromCache(CacheData cache)
171
  {
172
  int count = cache.Entries.Count;
173
  var store = new IndexStore
174
  {
175
- DriveRoot = cache.DriveRoot,
176
  Checkpoints = new List<JournalCheckpoint>(cache.Checkpoints)
177
  };
178
  store.Entries.Capacity = count;
179
  store.NameArena.Capacity = count * 30;
180
  store.NameLowerArena.Capacity = count * 30;
 
 
 
181
 
182
  foreach (var c in cache.Entries)
183
  {
184
  string nameLower = c.Name.ToLowerInvariant();
185
 
 
 
 
186
  uint nOff = (uint)store.NameArena.Count;
187
- ushort nLen = (ushort)c.Name.Length;
188
- store.NameArena.AddRange(Encoding.UTF8.GetBytes(c.Name));
189
 
190
  uint nlOff = (uint)store.NameLowerArena.Count;
191
- ushort nlLen = (ushort)nameLower.Length;
192
- store.NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower));
193
 
194
  store.Entries.Add(new IndexEntry
195
  {
@@ -199,8 +221,12 @@ public class IndexStore
199
  NameLowerOff = nlOff,
200
  NameLen = nLen,
201
  NameLowerLen = nlLen,
202
- Flags = c.Kind == FileKind.Directory ? (byte)1 : (byte)0
 
203
  });
 
 
 
204
  }
205
 
206
  store.RebuildRefLookup();
@@ -209,19 +235,24 @@ public class IndexStore
209
  return store;
210
  }
211
 
212
- // Live mutations β€” match Rust insert(), remove(), rename(), apply_move()
213
-
214
  public void Insert(FileRecord record)
215
  {
216
  string nameLower = record.Name.ToLowerInvariant();
217
 
 
 
 
218
  uint nOff = (uint)NameArena.Count;
219
- ushort nLen = (ushort)record.Name.Length;
220
- NameArena.AddRange(Encoding.UTF8.GetBytes(record.Name));
221
 
222
  uint nlOff = (uint)NameLowerArena.Count;
223
- ushort nlLen = (ushort)nameLower.Length;
224
- NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower));
 
 
 
 
225
 
226
  var entry = new IndexEntry
227
  {
@@ -231,27 +262,39 @@ public class IndexStore
231
  NameLowerOff = nlOff,
232
  NameLen = nLen,
233
  NameLowerLen = nlLen,
234
- Flags = record.Kind == FileKind.Directory ? (byte)1 : (byte)0
 
235
  };
236
 
237
- // Rust: partition_point by name_lower comparison
238
  int pos = 0;
239
- string key = nameLower;
240
  for (; pos < Entries.Count; pos++)
241
  {
242
- var e = Entries[pos];
243
- string cmp = Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)e.NameLowerOff, e.NameLowerLen);
244
- if (string.CompareOrdinal(key, cmp) < 0)
245
  break;
246
  }
247
  Entries.Insert(pos, entry);
 
 
 
248
  RebuildRefLookup();
249
  }
250
 
251
  public void Remove(ulong fileRef)
252
  {
253
- // Name bytes left as dead space in arena (negligible for rare deletes)
254
- Entries.RemoveAll(e => e.FileRef == fileRef);
 
 
 
 
 
 
 
 
 
 
 
 
255
  RebuildRefLookup();
256
  }
257
 
@@ -273,30 +316,34 @@ public class IndexStore
273
  });
274
  }
275
 
276
- // Build path by walking parent chain β€” matches Rust build_path()
277
  public string BuildPath(ulong fileRef)
278
  {
279
- var components = new List<string>(16);
 
280
  ulong current = fileRef;
 
281
 
282
  for (int i = 0; i < 64; i++)
283
  {
284
  var idx = LookupIdx(current);
285
  if (idx == null) break;
286
 
287
- var entry = Entries[(int)idx];
288
- components.Add(Name(entry));
 
 
289
  if (entry.ParentRef == current) break;
290
  current = entry.ParentRef;
291
  }
292
 
293
- components.Reverse();
294
- var sb = new StringBuilder(DriveRoot);
295
- foreach (var comp in components)
 
296
  {
297
  if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
298
  sb.Append('\\');
299
- sb.Append(comp);
300
  }
301
  return sb.ToString();
302
  }
 
12
  public ulong ParentRef { get; set; }
13
  public string Name { get; set; } = string.Empty;
14
  public FileKind Kind { get; set; }
15
+ public byte DriveIdx { get; set; }
16
  }
17
 
18
  [Serializable]
19
  public class CacheData
20
  {
21
  public List<CachedEntry> Entries { get; set; } = new();
22
+ public List<string> DriveRoots { get; set; } = new();
23
  public List<JournalCheckpoint> Checkpoints { get; set; } = new();
24
  }
25
 
 
26
  public struct IndexEntry
27
  {
28
  public ulong FileRef;
 
32
  public ushort NameLen;
33
  public ushort NameLowerLen;
34
  public byte Flags; // bit 0 = is_dir
35
+ public byte DriveIdx; // index into IndexStore.DriveRoots
36
 
37
  public readonly bool IsDir => (Flags & 1) != 0;
38
  public readonly FileKind Kind => IsDir ? FileKind.Directory : FileKind.File;
39
  }
40
 
 
41
  public class IndexStore
42
  {
43
  public List<IndexEntry> Entries = new();
44
+ public List<byte> NameArena = new();
45
+ public List<byte> NameLowerArena = new();
46
+ public List<string> NameCache = new();
47
+ public List<string> NameLowerCache = new();
48
+ public List<(ulong fileRef, int idx)> RefLookup = new();
49
+ public List<string> DriveRoots = new();
50
  public List<JournalCheckpoint> Checkpoints = new();
51
 
52
  public int Count => Entries.Count;
53
 
54
+ public string NameAt(int i) => NameCache[i];
55
+ public string NameLowerAt(int i) => NameLowerCache[i];
56
+ public string DriveRootAt(int i) => DriveRoots[Entries[i].DriveIdx];
 
 
 
 
 
 
 
57
 
 
58
  public uint? LookupIdx(ulong fileRef)
59
  {
60
  int lo = 0, hi = RefLookup.Count - 1;
 
78
  RefLookup.Sort((a, b) => a.fileRef.CompareTo(b.fileRef));
79
  }
80
 
 
81
  public void PopulateFromScan(ScanResult scan, string driveRoot)
82
  {
83
+ byte driveIdx = 0;
84
+ int existing = DriveRoots.IndexOf(driveRoot);
85
+ if (existing >= 0)
86
+ {
87
+ driveIdx = (byte)existing;
88
+ }
89
+ else
90
+ {
91
+ driveIdx = (byte)DriveRoots.Count;
92
+ DriveRoots.Add(driveRoot);
93
+ }
94
+
95
  int count = scan.Records.Count;
96
+ Entries.Capacity = Math.Max(Entries.Capacity, Entries.Count + count);
97
+ NameArena.Capacity = Math.Max(NameArena.Capacity, NameArena.Count + count * 30);
98
+ NameLowerArena.Capacity = Math.Max(NameLowerArena.Capacity, NameLowerArena.Count + count * 30);
99
+ NameCache.Capacity = Math.Max(NameCache.Capacity, NameCache.Count + count);
100
+ NameLowerCache.Capacity = Math.Max(NameLowerCache.Capacity, NameLowerCache.Count + count);
101
 
102
  foreach (var r in scan.Records)
103
  {
 
109
  string name = new string(nameChars);
110
  string nameLower = name.ToLowerInvariant();
111
 
112
+ byte[] nameBytes = Encoding.UTF8.GetBytes(name);
113
+ byte[] lowerBytes = Encoding.UTF8.GetBytes(nameLower);
114
+
115
  uint nOff = (uint)NameArena.Count;
116
+ ushort nLen = (ushort)nameBytes.Length;
117
+ NameArena.AddRange(nameBytes);
118
 
119
  uint nlOff = (uint)NameLowerArena.Count;
120
+ ushort nlLen = (ushort)lowerBytes.Length;
121
+ NameLowerArena.AddRange(lowerBytes);
122
 
123
  Entries.Add(new IndexEntry
124
  {
 
128
  NameLowerOff = nlOff,
129
  NameLen = nLen,
130
  NameLowerLen = nlLen,
131
+ Flags = r.IsDir ? (byte)1 : (byte)0,
132
+ DriveIdx = driveIdx
133
  });
134
+
135
+ NameCache.Add(name);
136
+ NameLowerCache.Add(nameLower);
137
  }
138
  }
139
 
140
+ /// <summary>
141
+ /// Sort entries by lowercase name and rebuild lookup tables.
142
+ /// Matches Rust IndexStore::finalize().
143
+ /// </summary>
144
+ public void CompleteIndex()
145
  {
 
 
146
  var indices = Enumerable.Range(0, Entries.Count).ToArray();
147
+ Array.Sort(indices, (a, b) => string.CompareOrdinal(NameLowerCache[a], NameLowerCache[b]));
148
+
149
+ var sortedEntries = new List<IndexEntry>(Entries.Count);
150
+ var sortedNames = new List<string>(Entries.Count);
151
+ var sortedLower = new List<string>(Entries.Count);
 
 
 
152
 
 
153
  foreach (var i in indices)
154
+ {
155
+ sortedEntries.Add(Entries[i]);
156
+ sortedNames.Add(NameCache[i]);
157
+ sortedLower.Add(NameLowerCache[i]);
158
+ }
159
 
160
+ Entries = sortedEntries;
161
+ NameCache = sortedNames;
162
+ NameLowerCache = sortedLower;
163
  RebuildRefLookup();
164
+
165
  NameArena.TrimExcess();
166
  NameLowerArena.TrimExcess();
167
  }
168
 
 
169
  public CacheData ToCache()
170
  {
171
  return new CacheData
172
  {
173
+ Entries = Entries.Select((e, i) => new CachedEntry
174
  {
175
  FileRef = e.FileRef,
176
  ParentRef = e.ParentRef,
177
+ Name = NameAt(i),
178
+ Kind = e.Kind,
179
+ DriveIdx = e.DriveIdx
180
  }).ToList(),
181
+ DriveRoots = new List<string>(DriveRoots),
182
  Checkpoints = new List<JournalCheckpoint>(Checkpoints)
183
  };
184
  }
185
 
 
186
  public static IndexStore FromCache(CacheData cache)
187
  {
188
  int count = cache.Entries.Count;
189
  var store = new IndexStore
190
  {
191
+ DriveRoots = new List<string>(cache.DriveRoots),
192
  Checkpoints = new List<JournalCheckpoint>(cache.Checkpoints)
193
  };
194
  store.Entries.Capacity = count;
195
  store.NameArena.Capacity = count * 30;
196
  store.NameLowerArena.Capacity = count * 30;
197
+ store.NameCache.Capacity = count;
198
+ store.NameLowerCache.Capacity = count;
199
+ store.RefLookup.Capacity = count;
200
 
201
  foreach (var c in cache.Entries)
202
  {
203
  string nameLower = c.Name.ToLowerInvariant();
204
 
205
+ byte[] nameBytes = Encoding.UTF8.GetBytes(c.Name);
206
+ byte[] lowerBytes = Encoding.UTF8.GetBytes(nameLower);
207
+
208
  uint nOff = (uint)store.NameArena.Count;
209
+ ushort nLen = (ushort)nameBytes.Length;
210
+ store.NameArena.AddRange(nameBytes);
211
 
212
  uint nlOff = (uint)store.NameLowerArena.Count;
213
+ ushort nlLen = (ushort)lowerBytes.Length;
214
+ store.NameLowerArena.AddRange(lowerBytes);
215
 
216
  store.Entries.Add(new IndexEntry
217
  {
 
221
  NameLowerOff = nlOff,
222
  NameLen = nLen,
223
  NameLowerLen = nlLen,
224
+ Flags = c.Kind == FileKind.Directory ? (byte)1 : (byte)0,
225
+ DriveIdx = c.DriveIdx
226
  });
227
+
228
+ store.NameCache.Add(c.Name);
229
+ store.NameLowerCache.Add(nameLower);
230
  }
231
 
232
  store.RebuildRefLookup();
 
235
  return store;
236
  }
237
 
 
 
238
  public void Insert(FileRecord record)
239
  {
240
  string nameLower = record.Name.ToLowerInvariant();
241
 
242
+ var nameBytes = Encoding.UTF8.GetBytes(record.Name);
243
+ var lowerBytes = Encoding.UTF8.GetBytes(nameLower);
244
+
245
  uint nOff = (uint)NameArena.Count;
246
+ ushort nLen = (ushort)nameBytes.Length;
247
+ NameArena.AddRange(nameBytes);
248
 
249
  uint nlOff = (uint)NameLowerArena.Count;
250
+ ushort nlLen = (ushort)lowerBytes.Length;
251
+ NameLowerArena.AddRange(lowerBytes);
252
+
253
+ byte driveIdx = 0;
254
+ if (DriveRoots.Count == 0)
255
+ DriveRoots.Add("C:\\");
256
 
257
  var entry = new IndexEntry
258
  {
 
262
  NameLowerOff = nlOff,
263
  NameLen = nLen,
264
  NameLowerLen = nlLen,
265
+ Flags = record.Kind == FileKind.Directory ? (byte)1 : (byte)0,
266
+ DriveIdx = driveIdx
267
  };
268
 
 
269
  int pos = 0;
 
270
  for (; pos < Entries.Count; pos++)
271
  {
272
+ if (string.CompareOrdinal(nameLower, NameLowerAt(pos)) < 0)
 
 
273
  break;
274
  }
275
  Entries.Insert(pos, entry);
276
+ NameCache.Insert(pos, record.Name);
277
+ NameLowerCache.Insert(pos, nameLower);
278
+
279
  RebuildRefLookup();
280
  }
281
 
282
  public void Remove(ulong fileRef)
283
  {
284
+ int idx = -1;
285
+ for (int i = 0; i < Entries.Count; i++)
286
+ {
287
+ if (Entries[i].FileRef == fileRef)
288
+ {
289
+ idx = i;
290
+ break;
291
+ }
292
+ }
293
+ if (idx < 0) return;
294
+
295
+ Entries.RemoveAt(idx);
296
+ NameCache.RemoveAt(idx);
297
+ NameLowerCache.RemoveAt(idx);
298
  RebuildRefLookup();
299
  }
300
 
 
316
  });
317
  }
318
 
 
319
  public string BuildPath(ulong fileRef)
320
  {
321
+ string[] components = new string[64];
322
+ int compCount = 0;
323
  ulong current = fileRef;
324
+ byte driveIdx = 0;
325
 
326
  for (int i = 0; i < 64; i++)
327
  {
328
  var idx = LookupIdx(current);
329
  if (idx == null) break;
330
 
331
+ int eIdx = (int)idx;
332
+ components[compCount++] = NameAt(eIdx);
333
+ var entry = Entries[eIdx];
334
+ driveIdx = entry.DriveIdx;
335
  if (entry.ParentRef == current) break;
336
  current = entry.ParentRef;
337
  }
338
 
339
+ string driveRoot = driveIdx < DriveRoots.Count ? DriveRoots[driveIdx] : "C:\\";
340
+ var sb = new StringBuilder(driveRoot.Length + compCount * 32);
341
+ sb.Append(driveRoot);
342
+ for (int i = compCount - 1; i >= 0; i--)
343
  {
344
  if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
345
  sb.Append('\\');
346
+ sb.Append(components[i]);
347
  }
348
  return sb.ToString();
349
  }
FastSeekWpf/Core/MftReader.cs CHANGED
@@ -9,7 +9,7 @@ namespace FastSeekWpf.Core;
9
 
10
  public class MftReader : IDisposable
11
  {
12
- private readonly IntPtr _handle;
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
- _handle = Win32Api.CreateFileW(
25
  drive.DevicePath,
26
  Win32Api.GENERIC_READ,
27
  Win32Api.FILE_SHARE_READ | Win32Api.FILE_SHARE_WRITE | Win32Api.FILE_SHARE_DELETE,
@@ -30,14 +30,17 @@ public class MftReader : IDisposable
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,7 +49,10 @@ public class MftReader : IDisposable
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,13 +113,13 @@ public class MftReader : IDisposable
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,7 +133,11 @@ public class MftReader : IDisposable
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,14 +157,16 @@ public class MftReader : IDisposable
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,13 +178,13 @@ public class MftReader : IDisposable
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,14 +249,18 @@ public class MftReader : IDisposable
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(_handle, 0, out _, 0);
 
244
 
245
  byte[] boot = new byte[512];
246
  uint br = 0;
247
- bool ok = Win32Api.ReadFile(_handle, boot, 512, out br, IntPtr.Zero);
248
  if (!ok || br < 512)
249
  return null;
250
 
@@ -262,7 +278,10 @@ public class MftReader : IDisposable
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,7 +305,7 @@ public class MftReader : IDisposable
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,7 +313,9 @@ public class MftReader : IDisposable
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,
@@ -401,7 +422,7 @@ public class MftReader : IDisposable
401
  {
402
  if (!_disposed)
403
  {
404
- Win32Api.CloseHandle(_handle);
405
  _disposed = true;
406
  }
407
  GC.SuppressFinalize(this);
 
9
 
10
  public class MftReader : IDisposable
11
  {
12
+ private readonly IntPtr _volumeHandle;
13
  private readonly NtfsDrive _drive;
14
  private bool _disposed;
15
 
 
21
  public MftReader(NtfsDrive drive)
22
  {
23
  _drive = drive;
24
+ _volumeHandle = 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 (_volumeHandle == 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
+ // ------------------------------------------------------------------
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
  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
  offset += recordSize.Value;
114
  }
115
 
116
+ // Align down to record boundary (matches Rust: offset = total - (total % record_size))
117
  offset = total - (total % recordSize.Value);
118
 
119
  leftover = total - offset;
120
  if (leftover > 0)
121
  {
122
+ // Copy tail to front of buffer (matches Rust std::ptr::copy)
123
  Buffer.BlockCopy(buffer, offset, buffer, 0, leftover);
124
  }
125
  }
 
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
 
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
+ _volumeHandle,
170
  Win32Api.FSCTL_ENUM_USN_DATA,
171
  enumDataPtr,
172
  (uint)enumDataSize,
 
178
  if (!ok)
179
  {
180
  int error = Marshal.GetLastWin32Error();
181
+ uint code = (uint)error;
182
+ if (code == 0x80070026) // ERROR_HANDLE_EOF
183
  {
184
+ Logger.Log($"[{Drive.Letter}] FSCTL_ENUM_USN_DATA: EOF after {iterations} iterations");
185
  break;
186
  }
187
+ if (error == 122) // ERROR_INSUFFICIENT_BUFFER
188
  {
189
  Logger.Log($"[{Drive.Letter}] FSCTL buffer too small ({buffer.Length}), retrying");
190
  buffer = new byte[buffer.Length * 2];
 
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];
262
  uint br = 0;
263
+ bool ok = Win32Api.ReadFile(_volumeHandle, boot, 512, out br, IntPtr.Zero);
264
  if (!ok || br < 512)
265
  return null;
266
 
 
278
  return recordSize;
279
  }
280
 
281
+ /// <summary>
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
  if (record[offset + end] != check0 || record[offset + end + 1] != check1)
306
  return false;
307
 
308
+ // RESTORE real bytes from fixup array (this is what Rust does)
309
  record[offset + end] = record[offset + fixupOff + i * 2];
310
  record[offset + end + 1] = record[offset + fixupOff + i * 2 + 1];
311
  }
 
313
  return true;
314
  }
315
 
316
+ /// <summary>
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,
 
422
  {
423
  if (!_disposed)
424
  {
425
+ Win32Api.CloseHandle(_volumeHandle);
426
  _disposed = true;
427
  }
428
  GC.SuppressFinalize(this);
FastSeekWpf/Core/SearchEngine.cs CHANGED
@@ -22,6 +22,21 @@ public class SearchResult
22
 
23
  public static class SearchEngine
24
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
26
  private static readonly string[] AppPathMarkers = {
27
  "\\program files\\", "\\program files (x86)\\",
@@ -73,7 +88,20 @@ public static class SearchEngine
73
  return false;
74
  }
75
 
76
- // Search β€” matches Rust search() exactly. Uses IndexStore.Name() / NameLower() / BuildPath()
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  public static List<SearchResult> Search(
78
  IndexStore store,
79
  string query,
@@ -81,58 +109,72 @@ public static class SearchEngine
81
  bool caseSensitive,
82
  List<string> excludedDirs)
83
  {
84
- if (string.IsNullOrEmpty(query))
85
  return new List<SearchResult>();
86
 
 
87
  string q = caseSensitive ? query : query.ToLowerInvariant();
 
88
 
89
- // Phase 1: lightweight name-only matching
90
- var candidates = new List<(int idx, byte rank)>();
91
 
92
- for (int i = 0; i < store.Entries.Count; i++)
93
  {
94
- var entry = store.Entries[i];
95
- string nameCmp = caseSensitive ? store.Name(entry) : store.NameLower(entry);
 
 
 
96
 
97
- byte rank;
98
- if (nameCmp == q) rank = 1;
99
- else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2;
100
- else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3;
101
- else continue;
102
 
103
- candidates.Add((i, rank));
 
 
104
  }
105
 
106
- // Phase 2: sort by rank, keep overshoot buffer
107
- candidates.Sort((a, b) => a.rank.CompareTo(b.rank));
108
  int overshoot = Math.Max(limit * 5, 1000);
109
- if (candidates.Count > overshoot)
110
- candidates.RemoveRange(overshoot, candidates.Count - overshoot);
111
 
112
- // Phase 3: build paths + exclusions + app promotion
113
- var results = new List<SearchResult>(limit);
114
 
115
- foreach (var (idx, baseRank) in candidates)
116
  {
117
  var entry = store.Entries[idx];
118
- string fullPath = store.BuildPath(entry.FileRef);
 
119
 
120
  if (excludedDirs.Count > 0)
121
  {
122
- string pathLower = fullPath.ToLowerInvariant();
123
- if (excludedDirs.Any(ex => pathLower.StartsWith(ex, StringComparison.Ordinal)))
124
- continue;
 
 
 
 
125
  }
126
 
127
- string nameLower = store.NameLower(entry);
 
 
128
  byte rank = baseRank;
129
  if (baseRank <= 2)
130
  {
131
- string? ext = nameLower.Contains('.') ? nameLower[(nameLower.LastIndexOf('.') + 1)..] : null;
132
- if (ext != null && Array.IndexOf(AppExtensions, ext) >= 0)
133
  {
134
- string pathLower = fullPath.ToLowerInvariant();
135
- if (ContainsAny(pathLower, AppPathMarkers))
136
  rank = 0;
137
  }
138
  }
@@ -140,7 +182,6 @@ public static class SearchEngine
140
  string? fileExt = null;
141
  if (!entry.IsDir)
142
  {
143
- string name = store.Name(entry);
144
  int dot = name.LastIndexOf('.');
145
  if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant();
146
  }
@@ -148,10 +189,10 @@ public static class SearchEngine
148
  results.Add(new SearchResult
149
  {
150
  FullPath = fullPath,
151
- Name = store.Name(entry),
152
  Rank = rank,
153
  IsDir = entry.IsDir,
154
- Kind = GetKind(fileExt, entry.IsDir, fullPath.ToLowerInvariant())
155
  });
156
  }
157
 
@@ -159,10 +200,12 @@ public static class SearchEngine
159
  if (results.Count > limit)
160
  results.RemoveRange(limit, results.Count - limit);
161
 
 
 
 
162
  return results;
163
  }
164
 
165
- // Extension search β€” matches Rust extension filtering in main.rs
166
  public static List<SearchResult> SearchByExtension(
167
  IndexStore store,
168
  string ext,
@@ -170,40 +213,76 @@ public static class SearchEngine
170
  {
171
  string dotExt = "." + ext.ToLowerInvariant();
172
  var results = new List<SearchResult>();
 
173
 
174
- for (int i = 0; i < store.Entries.Count; i++)
175
  {
176
- var entry = store.Entries[i];
177
- string name = store.NameLower(entry);
178
- if (!name.EndsWith(dotExt)) continue;
 
179
 
180
- string fullPath = store.BuildPath(entry.FileRef);
181
- string fullPathLower = fullPath.ToLowerInvariant();
 
182
 
183
- bool excluded = false;
184
- foreach (var ex in excludedDirs)
185
- {
186
- if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
187
- { excluded = true; break; }
188
- }
189
- if (excluded) continue;
190
 
191
- string? fileExt = null;
192
- int dot = name.LastIndexOf('.');
193
- if (dot >= 0) fileExt = name[(dot + 1)..];
194
 
195
- results.Add(new SearchResult
196
- {
197
- FullPath = fullPath,
198
- Name = store.Name(entry),
199
- Rank = 0,
200
- IsDir = entry.IsDir,
201
- Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
202
- });
203
 
204
- if (results.Count >= 50) break;
 
205
  }
206
 
207
  return results;
208
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
 
22
 
23
  public static class SearchEngine
24
  {
25
+ [ThreadStatic]
26
+ private static StringBuilder? _pathBuilder;
27
+
28
+ private static StringBuilder GetPathBuilder()
29
+ {
30
+ var sb = _pathBuilder;
31
+ if (sb == null)
32
+ {
33
+ sb = new StringBuilder(512);
34
+ _pathBuilder = sb;
35
+ }
36
+ sb.Clear();
37
+ return sb;
38
+ }
39
+
40
  private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" };
41
  private static readonly string[] AppPathMarkers = {
42
  "\\program files\\", "\\program files (x86)\\",
 
88
  return false;
89
  }
90
 
91
+ private static bool SpanContains(ReadOnlySpan<char> haystack, ReadOnlySpan<char> needle)
92
+ {
93
+ if (needle.IsEmpty) return true;
94
+ if (needle.Length > haystack.Length) return false;
95
+
96
+ int limit = haystack.Length - needle.Length + 1;
97
+ for (int i = 0; i < limit; i++)
98
+ {
99
+ if (haystack.Slice(i, needle.Length).SequenceEqual(needle))
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
  public static List<SearchResult> Search(
106
  IndexStore store,
107
  string query,
 
109
  bool caseSensitive,
110
  List<string> excludedDirs)
111
  {
112
+ if (string.IsNullOrWhiteSpace(query))
113
  return new List<SearchResult>();
114
 
115
+ var searchSw = System.Diagnostics.Stopwatch.StartNew();
116
  string q = caseSensitive ? query : query.ToLowerInvariant();
117
+ ReadOnlySpan<char> qSpan = q.AsSpan();
118
 
119
+ Span<(int idx, byte rank)> candidates = stackalloc (int, byte)[10000];
120
+ int candidateCount = 0;
121
 
122
+ lock (store)
123
  {
124
+ int count = store.Entries.Count;
125
+ for (int i = 0; i < count; i++)
126
+ {
127
+ string name = caseSensitive ? store.NameAt(i) : store.NameLowerAt(i);
128
+ ReadOnlySpan<char> nameSpan = name.AsSpan();
129
 
130
+ byte rank;
131
+ if (nameSpan.SequenceEqual(qSpan)) rank = 1;
132
+ else if (nameSpan.StartsWith(qSpan, StringComparison.Ordinal)) rank = 2;
133
+ else if (SpanContains(nameSpan, qSpan)) rank = 3;
134
+ else continue;
135
 
136
+ if (candidateCount < candidates.Length)
137
+ candidates[candidateCount++] = (i, rank);
138
+ }
139
  }
140
 
141
+ candidates[..candidateCount].Sort((a, b) => a.rank.CompareTo(b.rank));
142
+
143
  int overshoot = Math.Max(limit * 5, 1000);
144
+ if (candidateCount > overshoot)
145
+ candidateCount = overshoot;
146
 
147
+ var results = new List<SearchResult>(Math.Min(limit, candidateCount));
148
+ var pathBuilder = GetPathBuilder();
149
 
150
+ foreach (var (idx, baseRank) in candidates[..candidateCount])
151
  {
152
  var entry = store.Entries[idx];
153
+ string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
154
+ string fullPathLower = fullPath.ToLowerInvariant();
155
 
156
  if (excludedDirs.Count > 0)
157
  {
158
+ bool excluded = false;
159
+ foreach (var ex in excludedDirs)
160
+ {
161
+ if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
162
+ { excluded = true; break; }
163
+ }
164
+ if (excluded) continue;
165
  }
166
 
167
+ string name = store.NameAt(idx);
168
+ string nameLower = store.NameLowerAt(idx);
169
+
170
  byte rank = baseRank;
171
  if (baseRank <= 2)
172
  {
173
+ int dotIdx = nameLower.LastIndexOf('.');
174
+ if (dotIdx >= 0)
175
  {
176
+ string ext = nameLower[(dotIdx + 1)..];
177
+ if (Array.IndexOf(AppExtensions, ext) >= 0 && ContainsAny(fullPathLower, AppPathMarkers))
178
  rank = 0;
179
  }
180
  }
 
182
  string? fileExt = null;
183
  if (!entry.IsDir)
184
  {
 
185
  int dot = name.LastIndexOf('.');
186
  if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant();
187
  }
 
189
  results.Add(new SearchResult
190
  {
191
  FullPath = fullPath,
192
+ Name = name,
193
  Rank = rank,
194
  IsDir = entry.IsDir,
195
+ Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
196
  });
197
  }
198
 
 
200
  if (results.Count > limit)
201
  results.RemoveRange(limit, results.Count - limit);
202
 
203
+ searchSw.Stop();
204
+ Logger.Log($"Search '{query}': {candidateCount} candidates, {results.Count} results, {searchSw.ElapsedMilliseconds}ms (index={store.Count:N0})");
205
+
206
  return results;
207
  }
208
 
 
209
  public static List<SearchResult> SearchByExtension(
210
  IndexStore store,
211
  string ext,
 
213
  {
214
  string dotExt = "." + ext.ToLowerInvariant();
215
  var results = new List<SearchResult>();
216
+ var pathBuilder = GetPathBuilder();
217
 
218
+ lock (store)
219
  {
220
+ for (int i = 0; i < store.Entries.Count; i++)
221
+ {
222
+ string name = store.NameLowerAt(i);
223
+ if (!name.EndsWith(dotExt)) continue;
224
 
225
+ var entry = store.Entries[i];
226
+ string fullPath = BuildPathFast(store, entry.FileRef, pathBuilder, entry.DriveIdx);
227
+ string fullPathLower = fullPath.ToLowerInvariant();
228
 
229
+ bool excluded = false;
230
+ foreach (var ex in excludedDirs)
231
+ {
232
+ if (fullPathLower.StartsWith(ex, StringComparison.Ordinal))
233
+ { excluded = true; break; }
234
+ }
235
+ if (excluded) continue;
236
 
237
+ string? fileExt = null;
238
+ int dot = name.LastIndexOf('.');
239
+ if (dot >= 0) fileExt = name[(dot + 1)..];
240
 
241
+ results.Add(new SearchResult
242
+ {
243
+ FullPath = fullPath,
244
+ Name = store.NameAt(i),
245
+ Rank = 0,
246
+ IsDir = entry.IsDir,
247
+ Kind = GetKind(fileExt, entry.IsDir, fullPathLower)
248
+ });
249
 
250
+ if (results.Count >= 50) break;
251
+ }
252
  }
253
 
254
  return results;
255
  }
256
+
257
+ private static string BuildPathFast(IndexStore store, ulong fileRef, StringBuilder sb, byte startDriveIdx)
258
+ {
259
+ sb.Clear();
260
+ string[] components = new string[64];
261
+ int compCount = 0;
262
+ ulong current = fileRef;
263
+ byte driveIdx = startDriveIdx;
264
+
265
+ for (int i = 0; i < 64; i++)
266
+ {
267
+ var idx = store.LookupIdx(current);
268
+ if (idx == null) break;
269
+
270
+ int eIdx = (int)idx;
271
+ components[compCount++] = store.NameAt(eIdx);
272
+ var entry = store.Entries[eIdx];
273
+ driveIdx = entry.DriveIdx;
274
+ if (entry.ParentRef == current) break;
275
+ current = entry.ParentRef;
276
+ }
277
+
278
+ string driveRoot = driveIdx < store.DriveRoots.Count ? store.DriveRoots[driveIdx] : "C:\\";
279
+ sb.Append(driveRoot);
280
+ for (int i = compCount - 1; i >= 0; i--)
281
+ {
282
+ if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/')
283
+ sb.Append('\\');
284
+ sb.Append(components[i]);
285
+ }
286
+ return sb.ToString();
287
+ }
288
  }
FastSeekWpf/Core/UsnWatcher.cs CHANGED
@@ -102,33 +102,6 @@ public class UsnWatcher : IDisposable
102
  return count;
103
  }
104
 
105
- /// <summary>Synchronous run loop β€” matches Rust UsnWatcher::run()</summary>
106
- public void Run()
107
- {
108
- byte[] buffer = new byte[65536];
109
- while (!_disposed)
110
- {
111
- Thread.Sleep(500);
112
- Poll();
113
- }
114
- }
115
-
116
- /// <summary>Synchronous run with shared checkpoint updates β€” matches Rust UsnWatcher::run_shared()</summary>
117
- public void RunShared(List<JournalCheckpoint> sharedCheckpoints)
118
- {
119
- byte[] buffer = new byte[65536];
120
- while (!_disposed)
121
- {
122
- Thread.Sleep(500);
123
- Poll();
124
- lock (sharedCheckpoints)
125
- {
126
- sharedCheckpoints.RemoveAll(c => c.DriveLetter == _drive.Letter);
127
- sharedCheckpoints.Add(Checkpoint());
128
- }
129
- }
130
- }
131
-
132
  public async Task RunAsync(CancellationToken ct)
133
  {
134
  _running = true;
 
102
  return count;
103
  }
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  public async Task RunAsync(CancellationToken ct)
106
  {
107
  _running = true;
FastSeekWpf/MainWindow.xaml.cs CHANGED
@@ -164,7 +164,9 @@ public partial class MainWindow : Window, INotifyPropertyChanged
164
  {
165
  needFullScan = false;
166
  while (_eventQueue.TryDequeue(out var evt))
 
167
  ApplyEvent(evt);
 
168
  Logger.Log("Delta events applied.");
169
  }
170
  else
@@ -211,7 +213,7 @@ public partial class MainWindow : Window, INotifyPropertyChanged
211
  Logger.Log($"Scan failed for {drive.Letter}: {ex}");
212
  }
213
  }
214
- _index.Finalize();
215
  Logger.Log($"Full scan complete. Total indexed: {_index.Count}");
216
  CacheManager.SaveCache(_index);
217
  Logger.Log("Cache saved.");
@@ -275,7 +277,9 @@ public partial class MainWindow : Window, INotifyPropertyChanged
275
  if (processed > 100) break;
276
  }
277
  if (processed > 50)
 
278
  CacheManager.SaveCache(_index);
 
279
  Thread.Sleep(100);
280
  }
281
  }
@@ -312,7 +316,10 @@ public partial class MainWindow : Window, INotifyPropertyChanged
312
  return IntPtr.Zero;
313
  }
314
 
315
- private void Window_Deactivated(object sender, EventArgs e) => FadeOutAndHide();
 
 
 
316
 
317
  private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
318
  {
@@ -324,7 +331,11 @@ public partial class MainWindow : Window, INotifyPropertyChanged
324
  {
325
  if (Resources["FadeAnim"] is Storyboard fade)
326
  {
327
- fade.Completed += (s, e) => { Hide(); _isVisible = false; };
 
 
 
 
328
  fade.Begin(this);
329
  }
330
  else
@@ -337,7 +348,9 @@ public partial class MainWindow : Window, INotifyPropertyChanged
337
  private void ToggleVisibility()
338
  {
339
  if (_isVisible)
 
340
  FadeOutAndHide();
 
341
  else
342
  {
343
  CenterWindow();
@@ -349,9 +362,13 @@ public partial class MainWindow : Window, INotifyPropertyChanged
349
  SearchBox.Focus();
350
 
351
  if (Resources["AppearAnim"] is Storyboard appear)
 
352
  appear.Begin(this);
 
353
  else
 
354
  Opacity = 1;
 
355
  _isVisible = true;
356
  }
357
  }
@@ -395,7 +412,10 @@ public partial class MainWindow : Window, INotifyPropertyChanged
395
  }
396
  }
397
 
398
- private void SearchBox_Loaded(object sender, RoutedEventArgs e) => SearchBox.Focus();
 
 
 
399
 
400
  private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
401
  {
 
164
  {
165
  needFullScan = false;
166
  while (_eventQueue.TryDequeue(out var evt))
167
+ {
168
  ApplyEvent(evt);
169
+ }
170
  Logger.Log("Delta events applied.");
171
  }
172
  else
 
213
  Logger.Log($"Scan failed for {drive.Letter}: {ex}");
214
  }
215
  }
216
+ _index.CompleteIndex();
217
  Logger.Log($"Full scan complete. Total indexed: {_index.Count}");
218
  CacheManager.SaveCache(_index);
219
  Logger.Log("Cache saved.");
 
277
  if (processed > 100) break;
278
  }
279
  if (processed > 50)
280
+ {
281
  CacheManager.SaveCache(_index);
282
+ }
283
  Thread.Sleep(100);
284
  }
285
  }
 
316
  return IntPtr.Zero;
317
  }
318
 
319
+ private void Window_Deactivated(object sender, EventArgs e)
320
+ {
321
+ FadeOutAndHide();
322
+ }
323
 
324
  private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
325
  {
 
331
  {
332
  if (Resources["FadeAnim"] is Storyboard fade)
333
  {
334
+ fade.Completed += (s, e) =>
335
+ {
336
+ Hide();
337
+ _isVisible = false;
338
+ };
339
  fade.Begin(this);
340
  }
341
  else
 
348
  private void ToggleVisibility()
349
  {
350
  if (_isVisible)
351
+ {
352
  FadeOutAndHide();
353
+ }
354
  else
355
  {
356
  CenterWindow();
 
362
  SearchBox.Focus();
363
 
364
  if (Resources["AppearAnim"] is Storyboard appear)
365
+ {
366
  appear.Begin(this);
367
+ }
368
  else
369
+ {
370
  Opacity = 1;
371
+ }
372
  _isVisible = true;
373
  }
374
  }
 
412
  }
413
  }
414
 
415
+ private void SearchBox_Loaded(object sender, RoutedEventArgs e)
416
+ {
417
+ SearchBox.Focus();
418
+ }
419
 
420
  private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
421
  {
FastSeekWpf/NativeInterop/Win32Api.cs CHANGED
@@ -30,12 +30,14 @@ internal static class Win32Api
30
  public const uint OPEN_EXISTING = 3;
31
  public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
32
  public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
 
 
33
 
34
- // ── IOCTL codes ──
35
- // From Windows SDK winioctl.h. Verified against windows-rs crate.
36
- public const uint FSCTL_ENUM_USN_DATA = 0x000900B3;
37
- public const uint FSCTL_QUERY_USN_JOURNAL = 0x000900F4;
38
- public const uint FSCTL_READ_USN_JOURNAL = 0x000900BB;
39
 
40
  // USN reasons
41
  public const uint USN_REASON_FILE_CREATE = 0x00000100;
@@ -43,12 +45,9 @@ internal static class Win32Api
43
  public const uint USN_REASON_RENAME_OLD_NAME = 0x00001000;
44
  public const uint USN_REASON_RENAME_NEW_NAME = 0x00002000;
45
 
46
- // Win32 error codes (raw, NOT HRESULT)
47
- // Marshal.GetLastWin32Error() returns these raw codes.
48
- // Rust windows-rs wraps them as HRESULT (0x8007XXXX).
49
- public const int ERROR_HANDLE_EOF = 38; // Raw: 0x26
50
- public const int ERROR_INSUFFICIENT_BUFFER = 122; // Raw: 0x7A
51
- public const int ERROR_JOURNAL_NOT_ACTIVE = 1179; // Raw: 0x49B
52
 
53
  // DWM
54
  public const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
@@ -74,7 +73,7 @@ internal static class Win32Api
74
  out uint lpNumberOfBytesRead,
75
  IntPtr lpOverlapped);
76
 
77
- // Unsafe overload: read into a byte[] at a given offset β€” matches Rust ReadFile into &mut buffer[leftover..]
78
  public static unsafe bool ReadFile(
79
  IntPtr hFile,
80
  byte[] lpBuffer,
 
30
  public const uint OPEN_EXISTING = 3;
31
  public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
32
  public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
33
+ public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
34
+ public const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
35
 
36
+ // IOCTL codes
37
+ public const uint FSCTL_ENUM_USN_DATA = 0x900B03;
38
+ public const uint FSCTL_QUERY_USN_JOURNAL = 0x900F44;
39
+ public const uint FSCTL_READ_USN_JOURNAL = 0x900BB;
40
+ public const uint FSCTL_READ_FILE_USN_DATA = 0x900EB;
41
 
42
  // USN reasons
43
  public const uint USN_REASON_FILE_CREATE = 0x00000100;
 
45
  public const uint USN_REASON_RENAME_OLD_NAME = 0x00001000;
46
  public const uint USN_REASON_RENAME_NEW_NAME = 0x00002000;
47
 
48
+ // Error codes
49
+ public const uint ERROR_HANDLE_EOF = 0x80070026;
50
+ public const uint ERROR_JOURNAL_NOT_ACTIVE = 1179;
 
 
 
51
 
52
  // DWM
53
  public const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
 
73
  out uint lpNumberOfBytesRead,
74
  IntPtr lpOverlapped);
75
 
76
+ // Unsafe overload: read into a byte[] at a given offset via GCHandle
77
  public static unsafe bool ReadFile(
78
  IntPtr hFile,
79
  byte[] lpBuffer,
README.md CHANGED
@@ -1,4 +1,7 @@
1
-
 
 
 
2
  # FastSeek WPF
3
 
4
  A Windows-only WPF port of the FastSeek instant file search tool.
@@ -56,3 +59,11 @@ The app reads the NTFS Master File Table (MFT) directly via `FSCTL_ENUM_USN_DATA
56
  - [ ] Search results could use a virtualized `ListView` with container recycling for >10k visible items
57
  - [ ] No system tray icon β€” app runs hidden with hotkey toggle only
58
  - [ ] No settings UI for exclusions
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - ml-intern
4
+ ---
5
  # FastSeek WPF
6
 
7
  A Windows-only WPF port of the FastSeek instant file search tool.
 
59
  - [ ] Search results could use a virtualized `ListView` with container recycling for >10k visible items
60
  - [ ] No system tray icon β€” app runs hidden with hotkey toggle only
61
  - [ ] No settings UI for exclusions
62
+
63
+ <!-- ml-intern-provenance -->
64
+ ## Generated by ML Intern
65
+
66
+ This model repository was generated by [ML Intern](https://github.com/huggingface/ml-intern), an agent for machine learning research and development on the Hugging Face Hub.
67
+
68
+ - Try ML Intern: https://smolagents-ml-intern.hf.space
69
+ - Source code: https://github.com/huggingface/ml-intern