anshdadhich commited on
Commit
1c4658f
·
0 Parent(s):

initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .NET build artifacts
2
+ **/bin/
3
+ **/obj/
4
+ **/out/
5
+
6
+ # Visual Studio files
7
+ **/.vs/
8
+ **/*.user
9
+ **/*.suo
10
+ **/*.userprefs
11
+ **/*.sln.docstates
12
+
13
+ # Rust build artifacts
14
+ **/target/
15
+
16
+ # IDE files
17
+ **/.idea/
18
+ **/.vscode/
19
+ **/*.swp
20
+ **/*.swo
21
+
22
+ # Logs
23
+ **/*.log
24
+ **/logs/
25
+
26
+ # Temporary files
27
+ **/*.tmp
28
+ **/*.temp
29
+ **/temp/
30
+ **/tmp/
31
+
32
+ # OS files
33
+ **/.DS_Store
34
+ **/Thumbs.db
35
+
36
+ # NuGet
37
+ **/packages/
38
+ **/*.nupkg
39
+
40
+ # Test results
41
+ **/TestResults/
42
+ **/*.trx
43
+
44
+ # Coverage
45
+ **/coverage/
46
+ **/*.coverage
47
+ **/*.cobertura.xml
FastSeek.Cli/FastSeek.Cli.csproj ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>Exe</OutputType>
4
+ <TargetFramework>net8.0-windows</TargetFramework>
5
+ <ImplicitUsings>enable</ImplicitUsings>
6
+ <Nullable>enable</Nullable>
7
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8
+ </PropertyGroup>
9
+ <ItemGroup>
10
+ <ProjectReference Include="..\FastSeek.Core\FastSeek.Core.csproj" />
11
+ </ItemGroup>
12
+ </Project>
FastSeek.Cli/Program.cs ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Diagnostics;
2
+ using FastSeek.Core;
3
+
4
+ namespace FastSeek.Cli;
5
+
6
+ internal static class Program
7
+ {
8
+ private static void Main()
9
+ {
10
+ Console.WriteLine("FastSeek - starting...");
11
+ using var engine = new FastSeekEngine();
12
+ engine.InitializeAsync(msg => Console.WriteLine($"[startup] {msg}")).GetAwaiter().GetResult();
13
+
14
+ var configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "fastsearch", "config.txt");
15
+ Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
16
+ engine.ExcludedDirs.AddRange(LoadExclusions(configPath));
17
+
18
+ var resultLimit = 200;
19
+
20
+ Console.WriteLine("FastSeek - File Search");
21
+ PrintMetrics(engine);
22
+ Console.WriteLine("Commands: case, count, metrics, options, limit <n>, clearcache, exclusions, exclude <path>, unexclude <path>, quit");
23
+
24
+ while (true)
25
+ {
26
+ Console.Write("search> ");
27
+ var input = Console.ReadLine()?.Trim() ?? string.Empty;
28
+ if (string.IsNullOrEmpty(input)) continue;
29
+ if (input is "quit" or "exit" or "q") break;
30
+ if (input == "case") { engine.CaseSensitive = !engine.CaseSensitive; Console.WriteLine(engine.CaseSensitive ? "case sensitivity: ON" : "case sensitivity: OFF"); continue; }
31
+ if (input == "count") { Console.WriteLine($"{engine.Count():N0} files in index"); continue; }
32
+ if (input == "metrics") { PrintMetrics(engine); continue; }
33
+ if (input == "options") { Console.WriteLine($"case={(engine.CaseSensitive ? "on" : "off")}, limit={resultLimit}, exclusions={engine.ExcludedDirs.Count}, cache={engine.CachePath}"); continue; }
34
+ if (input == "clearcache")
35
+ {
36
+ try
37
+ {
38
+ if (File.Exists(engine.CachePath)) File.Delete(engine.CachePath);
39
+ Console.WriteLine($"cache cleared: {engine.CachePath}");
40
+ Console.WriteLine("restart FastSeek to force full rescan");
41
+ }
42
+ catch (Exception ex)
43
+ {
44
+ Console.WriteLine($"failed to clear cache: {ex.Message}");
45
+ }
46
+ continue;
47
+ }
48
+ if (input.StartsWith("limit ", StringComparison.OrdinalIgnoreCase))
49
+ {
50
+ if (int.TryParse(input[6..].Trim(), out var parsed) && parsed > 0 && parsed <= 5000)
51
+ {
52
+ resultLimit = parsed;
53
+ Console.WriteLine($"result limit set to {resultLimit}");
54
+ }
55
+ else Console.WriteLine("usage: limit <1..5000>");
56
+ continue;
57
+ }
58
+ if (input.StartsWith("exclude ", StringComparison.OrdinalIgnoreCase)) { var p = NormalizeExclude(input[8..]); if (!engine.ExcludedDirs.Contains(p)) engine.ExcludedDirs.Add(p); SaveExclusions(configPath, engine.ExcludedDirs); Console.WriteLine($"excluded: {p}"); continue; }
59
+ if (input.StartsWith("unexclude ", StringComparison.OrdinalIgnoreCase)) { var p = NormalizeExclude(input[10..]); engine.ExcludedDirs.RemoveAll(x => x == p); SaveExclusions(configPath, engine.ExcludedDirs); Console.WriteLine($"removed: {p}"); continue; }
60
+ if (input == "exclusions") { Console.WriteLine(engine.ExcludedDirs.Count == 0 ? "no excluded directories" : string.Join(Environment.NewLine, engine.ExcludedDirs.Select(x => $"- {x}"))); continue; }
61
+
62
+ var sw = Stopwatch.StartNew();
63
+ var results = engine.Search(input, resultLimit);
64
+ sw.Stop();
65
+
66
+ if (results.Count == 0) Console.WriteLine($"no results for \"{input}\"");
67
+ else
68
+ {
69
+ for (var i = 0; i < results.Count; i++) Console.WriteLine($"[{i + 1,4}] [{(results[i].IsDir ? "DIR " : "FILE")}] {results[i].FullPath}");
70
+ Console.WriteLine($"\nResults: {results.Count:N0} Time: {sw.Elapsed.TotalMilliseconds:F2} ms Limit: {resultLimit:N0}\n");
71
+ }
72
+ }
73
+ }
74
+
75
+ private static void PrintMetrics(FastSeekEngine engine)
76
+ {
77
+ var m = engine.LastStartupMetrics;
78
+ if (m is null) return;
79
+
80
+ Console.WriteLine();
81
+ Console.WriteLine("Startup Metrics");
82
+ Console.WriteLine("---------------");
83
+ Console.WriteLine($"Cache : {(m.CacheLoaded ? "HIT" : "MISS")}");
84
+ Console.WriteLine($"Total Files : {m.TotalFiles:N0}");
85
+ Console.WriteLine($"Drives : {m.DriveDiscoveryMs,10:F2} ms");
86
+ Console.WriteLine($"Cache Load : {m.CacheLoadMs,10:F2} ms");
87
+ Console.WriteLine($"Scan : {m.ScanMs,10:F2} ms");
88
+ Console.WriteLine($"Index : {m.IndexMs,10:F2} ms");
89
+ Console.WriteLine($"Cache Save : {m.CacheSaveMs,10:F2} ms");
90
+ Console.WriteLine($"Startup Total: {m.TotalStartupMs,10:F2} ms");
91
+ Console.WriteLine($"Cache Path : {engine.CachePath}");
92
+ Console.WriteLine();
93
+ }
94
+
95
+ private static List<string> LoadExclusions(string path) => File.Exists(path) ? File.ReadAllLines(path).Select(l => l.Trim().ToLowerInvariant()).Where(l => l.Length > 0).ToList() : [];
96
+ private static void SaveExclusions(string path, List<string> dirs) => File.WriteAllText(path, string.Join(Environment.NewLine, dirs));
97
+ private static string NormalizeExclude(string path)
98
+ {
99
+ var p = path.Trim().ToLowerInvariant();
100
+ return p.EndsWith("\\") || p.EndsWith("/") ? p : p + "\\";
101
+ }
102
+ }
FastSeek.Core/Drives.cs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Text;
2
+ using FastSeek.Core.Mft;
3
+
4
+ namespace FastSeek.Core.Utils;
5
+
6
+ public static class Drives
7
+ {
8
+ public static List<NtfsDrive> GetNtfsDrives()
9
+ {
10
+ var drives = new List<NtfsDrive>();
11
+ var buffer = new char[256];
12
+ var len = NativeMethods.GetLogicalDriveStrings((uint)buffer.Length, buffer);
13
+ if (len == 0) return drives;
14
+
15
+ var all = new string(buffer, 0, (int)len).Split('\0', StringSplitOptions.RemoveEmptyEntries);
16
+ foreach (var root in all)
17
+ {
18
+ if (!IsNtfs(root)) continue;
19
+ var letter = root[0];
20
+ drives.Add(new NtfsDrive
21
+ {
22
+ Letter = letter,
23
+ Root = root,
24
+ DevicePath = $"\\\\.\\{letter}:"
25
+ });
26
+ }
27
+ return drives;
28
+ }
29
+
30
+ private static bool IsNtfs(string root)
31
+ {
32
+ var fs = new char[32];
33
+ var ok = NativeMethods.GetVolumeInformation(root, null, 0, out _, out _, out _, fs, (uint)fs.Length);
34
+ if (!ok) return false;
35
+ var fsName = new string(fs).TrimEnd('\0');
36
+ return fsName.StartsWith("NTFS", StringComparison.OrdinalIgnoreCase);
37
+ }
38
+ }
FastSeek.Core/FastSeek.Core.csproj ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <TargetFramework>net8.0-windows</TargetFramework>
4
+ <ImplicitUsings>enable</ImplicitUsings>
5
+ <Nullable>enable</Nullable>
6
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
7
+ </PropertyGroup>
8
+ </Project>
FastSeek.Core/FastSeekEngine.cs ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Collections.Concurrent;
2
+ using System.Diagnostics;
3
+ using FastSeek.Core.Index;
4
+ using FastSeek.Core.Mft;
5
+ using FastSeek.Core.Search;
6
+ using FastSeek.Core.Utils;
7
+
8
+ namespace FastSeek.Core;
9
+
10
+ public sealed class StartupMetrics
11
+ {
12
+ public bool CacheLoaded { get; init; }
13
+ public int TotalFiles { get; init; }
14
+ public double DriveDiscoveryMs { get; init; }
15
+ public double ScanMs { get; init; }
16
+ public double IndexMs { get; init; }
17
+ public double CacheLoadMs { get; init; }
18
+ public double CacheSaveMs { get; init; }
19
+ public double TotalStartupMs { get; init; }
20
+ }
21
+
22
+ public sealed class FastSeekEngine : IDisposable
23
+ {
24
+ private readonly ReaderWriterLockSlim _storeLock = new();
25
+ private readonly BlockingCollection<IndexEvent> _queue = new();
26
+ private readonly CancellationTokenSource _cts = new();
27
+ private readonly object _checkpointLock = new();
28
+ private readonly string _cachePath = Path.Combine(Path.GetTempPath(), "fastseek_cache.bin");
29
+
30
+ private readonly List<JournalCheckpoint> _checkpoints = new();
31
+ private Task? _eventTask;
32
+ private readonly List<Task> _watcherTasks = new();
33
+
34
+ public IndexStore Store { get; } = new();
35
+ public bool CaseSensitive { get; set; }
36
+ public List<string> ExcludedDirs { get; } = new();
37
+ public StartupMetrics? LastStartupMetrics { get; private set; }
38
+ public string CachePath => _cachePath;
39
+
40
+ public async Task InitializeAsync(Action<string>? log = null)
41
+ {
42
+ log?.Invoke("Initializing FastSeek engine...");
43
+ var swTotal = Stopwatch.StartNew();
44
+
45
+ var swDrives = Stopwatch.StartNew();
46
+ var drives = Drives.GetNtfsDrives();
47
+ swDrives.Stop();
48
+ log?.Invoke($"Detected NTFS drives: {string.Join(", ", drives.Select(d => $"{d.Letter}:"))}");
49
+ if (drives.Count == 0) throw new InvalidOperationException("No NTFS drives found.");
50
+
51
+ var swCacheLoad = Stopwatch.StartNew();
52
+ log?.Invoke($"Loading cache: {_cachePath}");
53
+ var loaded = TryLoadCache(_cachePath);
54
+ swCacheLoad.Stop();
55
+ log?.Invoke(loaded ? "Cache hit. Loaded index from cache." : "Cache miss. Running full MFT scan...");
56
+
57
+ double scanMs = 0;
58
+ double indexMs = 0;
59
+ double cacheSaveMs = 0;
60
+
61
+ if (!loaded)
62
+ {
63
+ var swScan = Stopwatch.StartNew();
64
+ foreach (var d in drives)
65
+ {
66
+ log?.Invoke($"Scanning drive {d.Letter}:");
67
+ using var reader = MftReader.Open(d);
68
+ var direct = reader.ScanDirect(TimeSpan.FromSeconds(6), m => log?.Invoke($"[{d.Letter}:direct] {m}"));
69
+ var scan = direct ?? reader.Scan(m => log?.Invoke($"[{d.Letter}:ioctl] {m}"));
70
+ var method = direct is null ? "ioctl (fallback)" : "direct";
71
+ _storeLock.EnterWriteLock();
72
+ try { Store.PopulateFromScan(scan, d.Root); } finally { _storeLock.ExitWriteLock(); }
73
+ log?.Invoke($"Finished drive {d.Letter}: {scan.Records.Count:N0} records ({method})");
74
+ }
75
+ swScan.Stop();
76
+ scanMs = swScan.Elapsed.TotalMilliseconds;
77
+
78
+ var swIndex = Stopwatch.StartNew();
79
+ _storeLock.EnterWriteLock();
80
+ try { Store.FinalizeStore(); } finally { _storeLock.ExitWriteLock(); }
81
+ swIndex.Stop();
82
+ indexMs = swIndex.Elapsed.TotalMilliseconds;
83
+
84
+ var swCacheSave = Stopwatch.StartNew();
85
+ log?.Invoke("Saving cache...");
86
+ SaveCache(_cachePath);
87
+ swCacheSave.Stop();
88
+ cacheSaveMs = swCacheSave.Elapsed.TotalMilliseconds;
89
+ log?.Invoke("Cache saved.");
90
+ }
91
+
92
+ _storeLock.EnterReadLock();
93
+ try { _checkpoints.AddRange(Store.Checkpoints); }
94
+ finally { _storeLock.ExitReadLock(); }
95
+
96
+ foreach (var d in drives)
97
+ {
98
+ var drive = d;
99
+ _watcherTasks.Add(Task.Run(() =>
100
+ {
101
+ try
102
+ {
103
+ using var watcher = UsnWatcher.New(drive, _queue);
104
+ watcher.RunShared(_checkpoints, _checkpointLock, _cts.Token);
105
+ }
106
+ catch { }
107
+ }, _cts.Token));
108
+ }
109
+
110
+ _eventTask = Task.Run(() =>
111
+ {
112
+ foreach (var ev in _queue.GetConsumingEnumerable(_cts.Token))
113
+ {
114
+ _storeLock.EnterWriteLock();
115
+ try
116
+ {
117
+ switch (ev)
118
+ {
119
+ case IndexEvent.Created c: Store.Insert(c.Record); break;
120
+ case IndexEvent.Deleted d: Store.Remove(d.FileRef); break;
121
+ case IndexEvent.Renamed r: Store.Rename(r.OldRef, r.NewRecord); break;
122
+ case IndexEvent.Moved m: Store.ApplyMove(m.FileRef, m.NewParentRef, m.Name, m.Kind); break;
123
+ }
124
+ }
125
+ finally { _storeLock.ExitWriteLock(); }
126
+ }
127
+ }, _cts.Token);
128
+
129
+ swTotal.Stop();
130
+ LastStartupMetrics = new StartupMetrics
131
+ {
132
+ CacheLoaded = loaded,
133
+ TotalFiles = Count(),
134
+ DriveDiscoveryMs = swDrives.Elapsed.TotalMilliseconds,
135
+ ScanMs = scanMs,
136
+ IndexMs = indexMs,
137
+ CacheLoadMs = swCacheLoad.Elapsed.TotalMilliseconds,
138
+ CacheSaveMs = cacheSaveMs,
139
+ TotalStartupMs = swTotal.Elapsed.TotalMilliseconds,
140
+ };
141
+ log?.Invoke($"Startup complete in {LastStartupMetrics.TotalStartupMs:F2} ms");
142
+
143
+ await Task.CompletedTask;
144
+ }
145
+
146
+ public List<SearchResult> Search(string input, int limit = 50)
147
+ {
148
+ if (string.IsNullOrWhiteSpace(input)) return [];
149
+ var parsed = ParseQuery(input.Trim());
150
+
151
+ _storeLock.EnterReadLock();
152
+ try
153
+ {
154
+ if (parsed.ExtFilter is not null)
155
+ {
156
+ var dot = "." + parsed.ExtFilter;
157
+ return Store.Entries.Where(e => e.NameLower.EndsWith(dot, StringComparison.Ordinal))
158
+ .Where(e => parsed.Filter == Filter.All || (parsed.Filter == Filter.Dirs && e.IsDir) || (parsed.Filter == Filter.Files && !e.IsDir))
159
+ .Select(e => new SearchResult { FullPath = SearchEngine.BuildPath(e.FileRef, Store), Name = e.Name, Rank = 0, IsDir = e.IsDir })
160
+ .Where(r => ExcludedDirs.Count == 0 || !ExcludedDirs.Any(ex => r.FullPath.ToLowerInvariant().StartsWith(ex, StringComparison.Ordinal)))
161
+ .Take(limit)
162
+ .ToList();
163
+ }
164
+
165
+ var overshootLimit = Math.Max(limit * 4, 400);
166
+ return SearchEngine.Search(Store, parsed.Query, overshootLimit, CaseSensitive, ExcludedDirs)
167
+ .Where(r => parsed.Filter == Filter.All || (parsed.Filter == Filter.Dirs && r.IsDir) || (parsed.Filter == Filter.Files && !r.IsDir))
168
+ .Take(limit)
169
+ .ToList();
170
+ }
171
+ finally { _storeLock.ExitReadLock(); }
172
+ }
173
+
174
+ public int Count()
175
+ {
176
+ _storeLock.EnterReadLock();
177
+ try { return Store.Len(); }
178
+ finally { _storeLock.ExitReadLock(); }
179
+ }
180
+
181
+ private bool TryLoadCache(string path)
182
+ {
183
+ if (!File.Exists(path)) return false;
184
+ try
185
+ {
186
+ using var fs = File.OpenRead(path);
187
+ using var br = new BinaryReader(fs, System.Text.Encoding.UTF8, leaveOpen: false);
188
+ var magic = br.ReadUInt32();
189
+ if (magic != 0x4B534146) return false; // FASK
190
+ var version = br.ReadInt32();
191
+ if (version != 1) return false;
192
+
193
+ var driveRoot = br.ReadString();
194
+ var checkpointCount = br.ReadInt32();
195
+ var checkpoints = new List<JournalCheckpoint>(checkpointCount);
196
+ for (var i = 0; i < checkpointCount; i++)
197
+ {
198
+ checkpoints.Add(new JournalCheckpoint
199
+ {
200
+ NextUsn = br.ReadInt64(),
201
+ JournalId = br.ReadUInt64(),
202
+ DriveLetter = br.ReadChar()
203
+ });
204
+ }
205
+
206
+ var entryCount = br.ReadInt32();
207
+ var cacheData = new CacheData
208
+ {
209
+ DriveRoot = driveRoot,
210
+ Checkpoints = checkpoints,
211
+ Entries = new List<CachedEntry>(entryCount)
212
+ };
213
+ for (var i = 0; i < entryCount; i++)
214
+ {
215
+ cacheData.Entries.Add(new CachedEntry
216
+ {
217
+ FileRef = br.ReadUInt64(),
218
+ ParentRef = br.ReadUInt64(),
219
+ Kind = br.ReadByte() == 1 ? FileKind.Directory : FileKind.File,
220
+ Name = br.ReadString()
221
+ });
222
+ }
223
+ var restored = IndexStore.FromCache(cacheData);
224
+
225
+ _storeLock.EnterWriteLock();
226
+ try
227
+ {
228
+ Store.Entries.Clear();
229
+ Store.RefLookup.Clear();
230
+ Store.DriveRoot = restored.DriveRoot;
231
+ Store.Checkpoints = restored.Checkpoints;
232
+ Store.Entries.AddRange(restored.Entries);
233
+ Store.RefLookup.AddRange(restored.RefLookup);
234
+ }
235
+ finally { _storeLock.ExitWriteLock(); }
236
+ return true;
237
+ }
238
+ catch { return false; }
239
+ }
240
+
241
+ private void SaveCache(string path)
242
+ {
243
+ try
244
+ {
245
+ _storeLock.EnterReadLock();
246
+ using var fs = File.Create(path);
247
+ using var bw = new BinaryWriter(fs, System.Text.Encoding.UTF8, leaveOpen: false);
248
+ bw.Write(0x4B534146u); // FASK
249
+ bw.Write(1); // version
250
+ bw.Write(Store.DriveRoot ?? string.Empty);
251
+ bw.Write(Store.Checkpoints.Count);
252
+ for (var i = 0; i < Store.Checkpoints.Count; i++)
253
+ {
254
+ var c = Store.Checkpoints[i];
255
+ bw.Write(c.NextUsn);
256
+ bw.Write(c.JournalId);
257
+ bw.Write(c.DriveLetter);
258
+ }
259
+
260
+ bw.Write(Store.Entries.Count);
261
+ for (var i = 0; i < Store.Entries.Count; i++)
262
+ {
263
+ var e = Store.Entries[i];
264
+ bw.Write(e.FileRef);
265
+ bw.Write(e.ParentRef);
266
+ bw.Write((byte)(e.IsDir ? 1 : 0));
267
+ bw.Write(e.Name);
268
+ }
269
+ }
270
+ catch { }
271
+ }
272
+
273
+ public void Dispose()
274
+ {
275
+ _cts.Cancel();
276
+ _queue.CompleteAdding();
277
+ lock (_checkpointLock)
278
+ {
279
+ _storeLock.EnterWriteLock();
280
+ try { Store.Checkpoints = _checkpoints.ToList(); } finally { _storeLock.ExitWriteLock(); }
281
+ }
282
+ SaveCache(_cachePath);
283
+ _storeLock.Dispose();
284
+ _queue.Dispose();
285
+ _cts.Dispose();
286
+ }
287
+
288
+ private enum Filter { All, Dirs, Files }
289
+ private sealed class ParsedQuery { public required string Query; public Filter Filter; public string? ExtFilter; }
290
+
291
+ private static ParsedQuery ParseQuery(string input)
292
+ {
293
+ if (input.StartsWith("ext:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = string.Empty, Filter = Filter.Files, ExtFilter = input[4..].ToLowerInvariant() };
294
+ if (input.StartsWith("*.", StringComparison.Ordinal)) return new ParsedQuery { Query = string.Empty, Filter = Filter.All, ExtFilter = input[2..].ToLowerInvariant() };
295
+ if (input.StartsWith("folder:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = input[7..].Trim(), Filter = Filter.Dirs };
296
+ if (input.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = input[5..].Trim(), Filter = Filter.Files };
297
+ if (input.StartsWith(':')) return new ParsedQuery { Query = input[1..], Filter = Filter.Dirs };
298
+ if (input.StartsWith('!')) return new ParsedQuery { Query = input[1..], Filter = Filter.Files };
299
+ return new ParsedQuery { Query = input, Filter = Filter.All };
300
+ }
301
+ }
FastSeek.Core/IndexStore.cs ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FastSeek.Core.Mft;
2
+
3
+ namespace FastSeek.Core.Index;
4
+
5
+ public sealed class IndexEntry
6
+ {
7
+ public ulong FileRef;
8
+ public ulong ParentRef;
9
+ public required string Name;
10
+ public required string NameLower;
11
+ public byte Flags;
12
+ public bool IsDir => (Flags & 1) != 0;
13
+ public FileKind Kind => IsDir ? FileKind.Directory : FileKind.File;
14
+ }
15
+
16
+ public sealed class CachedEntry
17
+ {
18
+ public ulong FileRef { get; set; }
19
+ public ulong ParentRef { get; set; }
20
+ public required string Name { get; set; }
21
+ public FileKind Kind { get; set; }
22
+ }
23
+
24
+ public sealed class CacheData
25
+ {
26
+ public List<CachedEntry> Entries { get; set; } = new();
27
+ public string DriveRoot { get; set; } = string.Empty;
28
+ public List<JournalCheckpoint> Checkpoints { get; set; } = new();
29
+ }
30
+
31
+ public sealed class IndexStore
32
+ {
33
+ public List<IndexEntry> Entries { get; } = new();
34
+ public List<(ulong fileRef, int idx)> RefLookup { get; } = new();
35
+ public string DriveRoot { get; set; } = string.Empty;
36
+ public List<JournalCheckpoint> Checkpoints { get; set; } = new();
37
+
38
+ public string Name(IndexEntry e) => e.Name;
39
+ public string NameLower(IndexEntry e) => e.NameLower;
40
+
41
+ public int? LookupIdx(ulong fileRef)
42
+ {
43
+ var lo = 0;
44
+ var hi = RefLookup.Count - 1;
45
+ while (lo <= hi)
46
+ {
47
+ var mid = lo + ((hi - lo) >> 1);
48
+ var cmp = RefLookup[mid].fileRef.CompareTo(fileRef);
49
+ if (cmp == 0) return RefLookup[mid].idx;
50
+ if (cmp < 0) lo = mid + 1; else hi = mid - 1;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ internal void PopulateFromScan(ScanResult scan, string driveRoot)
56
+ {
57
+ DriveRoot = driveRoot;
58
+ var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(scan.NameData);
59
+ Entries.Capacity = Math.Max(Entries.Capacity, Entries.Count + scan.Records.Count);
60
+ foreach (var r in scan.Records)
61
+ {
62
+ var name = new string(span.Slice((int)r.NameOff, r.NameLen));
63
+ AddEntry(r.FileRef, r.ParentRef, name, r.IsDir ? FileKind.Directory : FileKind.File);
64
+ }
65
+ }
66
+
67
+ public void FinalizeStore()
68
+ {
69
+ Entries.Sort(static (a, b) => string.CompareOrdinal(a.NameLower, b.NameLower));
70
+ RebuildRefLookup();
71
+ }
72
+
73
+ public void Insert(FileRecord record) => AddSorted(record.FileRef, record.ParentRef, record.Name, record.Kind);
74
+ public void Remove(ulong fileRef) { Entries.RemoveAll(e => e.FileRef == fileRef); RebuildRefLookup(); }
75
+ public void Rename(ulong oldRef, FileRecord r) { Remove(oldRef); Insert(r); }
76
+ public void ApplyMove(ulong fileRef, ulong newParentRef, string name, FileKind kind) { Remove(fileRef); Insert(new FileRecord { FileRef = fileRef, ParentRef = newParentRef, Name = name, Kind = kind }); }
77
+ public int Len() => Entries.Count;
78
+
79
+ public CacheData ToCache() => new()
80
+ {
81
+ DriveRoot = DriveRoot,
82
+ Checkpoints = Checkpoints,
83
+ Entries = Entries.Select(e => new CachedEntry { FileRef = e.FileRef, ParentRef = e.ParentRef, Name = e.Name, Kind = e.Kind }).ToList()
84
+ };
85
+
86
+ public static IndexStore FromCache(CacheData cache)
87
+ {
88
+ var s = new IndexStore { DriveRoot = cache.DriveRoot, Checkpoints = cache.Checkpoints };
89
+ s.Entries.Capacity = cache.Entries.Count;
90
+ foreach (var c in cache.Entries) s.AddEntry(c.FileRef, c.ParentRef, c.Name, c.Kind);
91
+ s.RebuildRefLookup();
92
+ return s;
93
+ }
94
+
95
+ private void AddSorted(ulong fileRef, ulong parentRef, string name, FileKind kind)
96
+ {
97
+ var lower = name.ToLowerInvariant();
98
+ var pos = Entries.BinarySearch(new IndexEntry { FileRef = fileRef, ParentRef = parentRef, Name = name, NameLower = lower, Flags = kind == FileKind.Directory ? (byte)1 : (byte)0 },
99
+ Comparer<IndexEntry>.Create(static (a, b) => string.CompareOrdinal(a.NameLower, b.NameLower)));
100
+ if (pos < 0) pos = ~pos;
101
+ Entries.Insert(pos, new IndexEntry { FileRef = fileRef, ParentRef = parentRef, Name = name, NameLower = lower, Flags = kind == FileKind.Directory ? (byte)1 : (byte)0 });
102
+ RebuildRefLookup();
103
+ }
104
+
105
+ private void AddEntry(ulong fileRef, ulong parentRef, string name, FileKind kind)
106
+ {
107
+ Entries.Add(new IndexEntry
108
+ {
109
+ FileRef = fileRef,
110
+ ParentRef = parentRef,
111
+ Name = name,
112
+ NameLower = name.ToLowerInvariant(),
113
+ Flags = kind == FileKind.Directory ? (byte)1 : (byte)0
114
+ });
115
+ }
116
+
117
+ private void RebuildRefLookup()
118
+ {
119
+ RefLookup.Clear();
120
+ RefLookup.Capacity = Math.Max(RefLookup.Capacity, Entries.Count);
121
+ for (var i = 0; i < Entries.Count; i++) RefLookup.Add((Entries[i].FileRef, i));
122
+ RefLookup.Sort(static (a, b) => a.fileRef.CompareTo(b.fileRef));
123
+ }
124
+ }
FastSeek.Core/MftReader.cs ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Runtime.InteropServices;
2
+ using Microsoft.Win32.SafeHandles;
3
+
4
+ namespace FastSeek.Core.Mft;
5
+
6
+ internal sealed class MftReader : IDisposable
7
+ {
8
+ private const int FallbackBuf = 4 * 1024 * 1024;
9
+ private const int DirectBuf = 4 * 1024 * 1024;
10
+
11
+ private readonly SafeFileHandle _handle;
12
+ public NtfsDrive Drive { get; }
13
+
14
+ private MftReader(SafeFileHandle handle, NtfsDrive drive)
15
+ {
16
+ _handle = handle;
17
+ Drive = drive;
18
+ }
19
+
20
+ public static MftReader Open(NtfsDrive drive)
21
+ {
22
+ var h = NativeMethods.CreateFile(drive.DevicePath, NativeMethods.GENERIC_READ, NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE, IntPtr.Zero, NativeMethods.OPEN_EXISTING, NativeMethods.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
23
+ if (h.IsInvalid) throw new IOException($"Open drive failed: {drive.DevicePath}");
24
+ return new MftReader(h, drive);
25
+ }
26
+
27
+ public ScanResult? ScanDirect(TimeSpan? maxDuration = null, Action<string>? log = null)
28
+ {
29
+ var started = DateTime.UtcNow;
30
+ var recordSize = ReadMftRecordSize();
31
+ if (recordSize is null)
32
+ {
33
+ log?.Invoke("direct: failed to read NTFS record size");
34
+ return null;
35
+ }
36
+ log?.Invoke($"direct: record size={recordSize.Value} bytes");
37
+
38
+ var mftPath = Path.Combine(Drive.Root, "$MFT");
39
+ var mft = NativeMethods.CreateFile(mftPath, NativeMethods.GENERIC_READ, NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE, IntPtr.Zero, NativeMethods.OPEN_EXISTING, NativeMethods.FILE_FLAG_BACKUP_SEMANTICS | NativeMethods.FILE_FLAG_SEQUENTIAL_SCAN, IntPtr.Zero);
40
+ if (mft.IsInvalid)
41
+ {
42
+ log?.Invoke("direct: could not open $MFT");
43
+ return null;
44
+ }
45
+
46
+ try
47
+ {
48
+ var records = new List<CompactRecord>(3_000_000);
49
+ var names = new List<char>(40_000_000);
50
+ var buffer = new byte[DirectBuf];
51
+ var readChunk = new byte[DirectBuf];
52
+ ulong mftIndex = 0;
53
+ var leftover = 0;
54
+ long totalRead = 0;
55
+ var lastLog = DateTime.UtcNow;
56
+
57
+ while (NativeMethods.ReadFile(mft, readChunk, (uint)(buffer.Length - leftover), out var read, IntPtr.Zero) && read > 0)
58
+ {
59
+ if (maxDuration.HasValue && (DateTime.UtcNow - started) > maxDuration.Value)
60
+ {
61
+ log?.Invoke($"direct: timed out after {(DateTime.UtcNow - started).TotalSeconds:F1}s, fallback to ioctl");
62
+ return null;
63
+ }
64
+ totalRead += read;
65
+ Buffer.BlockCopy(readChunk, 0, buffer, leftover, (int)read);
66
+ var total = leftover + (int)read;
67
+ var offset = 0;
68
+ while (offset + recordSize.Value <= total)
69
+ {
70
+ var slice = new Span<byte>(buffer, offset, recordSize.Value);
71
+ if (ApplyFixup(slice)) ParseFileRecord(slice, mftIndex, records, names);
72
+ mftIndex++;
73
+ offset += recordSize.Value;
74
+ }
75
+ offset = total - (total % recordSize.Value);
76
+ leftover = total - offset;
77
+ if (leftover > 0) Buffer.BlockCopy(buffer, offset, buffer, 0, leftover);
78
+
79
+ var now = DateTime.UtcNow;
80
+ if ((now - lastLog).TotalSeconds >= 2.0)
81
+ {
82
+ log?.Invoke($"direct: read={totalRead / (1024 * 1024)}MB parsed={records.Count:N0}");
83
+ lastLog = now;
84
+ }
85
+ }
86
+
87
+ log?.Invoke($"direct: completed read={totalRead / (1024 * 1024)}MB parsed={records.Count:N0}");
88
+ return new ScanResult { Records = records, NameData = names };
89
+ }
90
+ finally { mft.Dispose(); }
91
+ }
92
+
93
+ public ScanResult Scan(Action<string>? log = null)
94
+ {
95
+ var records = new List<CompactRecord>(3_000_000);
96
+ var names = new List<char>(40_000_000);
97
+ var enumData = new MftEnumDataV0 { StartFileReferenceNumber = 0, LowUsn = 0, HighUsn = long.MaxValue };
98
+ var outBuf = new byte[FallbackBuf];
99
+ var loops = 0;
100
+ var lastLog = DateTime.UtcNow;
101
+
102
+ while (true)
103
+ {
104
+ var pIn = Marshal.AllocHGlobal(Marshal.SizeOf<MftEnumDataV0>());
105
+ try
106
+ {
107
+ Marshal.StructureToPtr(enumData, pIn, false);
108
+ if (!NativeMethods.DeviceIoControl(_handle, NativeMethods.FSCTL_ENUM_USN_DATA, pIn, (uint)Marshal.SizeOf<MftEnumDataV0>(), outBuf, (uint)outBuf.Length, out var returned, IntPtr.Zero))
109
+ {
110
+ var err = Marshal.GetLastWin32Error();
111
+ log?.Invoke($"ioctl: DeviceIoControl stop err=0x{err:X8}");
112
+ break;
113
+ }
114
+ if (returned <= 8)
115
+ {
116
+ log?.Invoke("ioctl: no more records");
117
+ break;
118
+ }
119
+ loops++;
120
+
121
+ enumData.StartFileReferenceNumber = BitConverter.ToUInt64(outBuf, 0);
122
+ var offset = 8;
123
+ while (offset + Marshal.SizeOf<UsnRecordV2>() <= returned)
124
+ {
125
+ var rec = MemoryMarshal.Read<UsnRecordV2>(new ReadOnlySpan<byte>(outBuf, offset, Marshal.SizeOf<UsnRecordV2>()));
126
+ if (rec.RecordLength == 0 || offset + rec.RecordLength > returned) break;
127
+ var nameLen = (ushort)(rec.FileNameLength / 2);
128
+ var nameStart = offset + rec.FileNameOffset;
129
+ var chars = MemoryMarshal.Cast<byte, char>(new ReadOnlySpan<byte>(outBuf, nameStart, nameLen * 2));
130
+ var arenaOff = (uint)names.Count;
131
+ AppendChars(names, chars);
132
+ records.Add(new CompactRecord { FileRef = rec.FileReferenceNumber, ParentRef = rec.ParentFileReferenceNumber, NameOff = arenaOff, NameLen = nameLen, IsDir = (rec.FileAttributes & 0x10) != 0 });
133
+ offset += (int)rec.RecordLength;
134
+ }
135
+
136
+ var now = DateTime.UtcNow;
137
+ if ((now - lastLog).TotalSeconds >= 2.0)
138
+ {
139
+ log?.Invoke($"ioctl: loops={loops:N0} parsed={records.Count:N0} nextRef={enumData.StartFileReferenceNumber}");
140
+ lastLog = now;
141
+ }
142
+ }
143
+ finally { Marshal.FreeHGlobal(pIn); }
144
+ }
145
+
146
+ log?.Invoke($"ioctl: completed loops={loops:N0} parsed={records.Count:N0}");
147
+ return new ScanResult { Records = records, NameData = names };
148
+ }
149
+
150
+ private int? ReadMftRecordSize()
151
+ {
152
+ if (!NativeMethods.SetFilePointerEx(_handle, 0, out _, NativeMethods.FILE_BEGIN)) return null;
153
+ var boot = new byte[512];
154
+ if (!NativeMethods.ReadFile(_handle, boot, 512, out var read, IntPtr.Zero) || read < 512) return null;
155
+ if (boot[3] != (byte)'N' || boot[4] != (byte)'T' || boot[5] != (byte)'F' || boot[6] != (byte)'S') return null;
156
+ var bps = BitConverter.ToUInt16(boot, 0x0B);
157
+ var spc = boot[0x0D];
158
+ var cluster = bps * spc;
159
+ var raw = unchecked((sbyte)boot[0x40]);
160
+ return raw > 0 ? raw * cluster : 1 << -raw;
161
+ }
162
+
163
+ private static bool ApplyFixup(Span<byte> rec)
164
+ {
165
+ if (rec.Length < 48 || rec[0] != (byte)'F' || rec[1] != (byte)'I' || rec[2] != (byte)'L' || rec[3] != (byte)'E') return false;
166
+ var off = BitConverter.ToUInt16(rec.Slice(4, 2));
167
+ var cnt = BitConverter.ToUInt16(rec.Slice(6, 2));
168
+ if (cnt < 2 || off + cnt * 2 > rec.Length) return false;
169
+ var c0 = rec[off]; var c1 = rec[off + 1];
170
+ for (var i = 1; i < cnt; i++)
171
+ {
172
+ var end = i * 512 - 2;
173
+ if (end + 1 >= rec.Length) break;
174
+ if (rec[end] != c0 || rec[end + 1] != c1) return false;
175
+ rec[end] = rec[off + i * 2];
176
+ rec[end + 1] = rec[off + i * 2 + 1];
177
+ }
178
+ return true;
179
+ }
180
+
181
+ private static void ParseFileRecord(ReadOnlySpan<byte> rec, ulong mftIndex, List<CompactRecord> outRecords, List<char> outNames)
182
+ {
183
+ var flags = BitConverter.ToUInt16(rec.Slice(0x16, 2));
184
+ if ((flags & 0x01) == 0) return;
185
+ var isDir = (flags & 0x02) != 0;
186
+ var seq = BitConverter.ToUInt16(rec.Slice(0x10, 2));
187
+ var fileRef = mftIndex | ((ulong)seq << 48);
188
+ var firstAttr = BitConverter.ToUInt16(rec.Slice(0x14, 2));
189
+ var aoff = (int)firstAttr;
190
+ byte bestNs = 255;
191
+ (int pos, int len, ulong parent)? best = null;
192
+
193
+ while (aoff + 8 <= rec.Length)
194
+ {
195
+ var atype = BitConverter.ToUInt32(rec.Slice(aoff, 4));
196
+ if (atype == 0xFFFFFFFF) break;
197
+ var alen = (int)BitConverter.ToUInt32(rec.Slice(aoff + 4, 4));
198
+ if (alen == 0 || aoff + alen > rec.Length) break;
199
+ if (atype == 0x30 && rec[aoff + 8] == 0)
200
+ {
201
+ var voff = (int)BitConverter.ToUInt16(rec.Slice(aoff + 20, 2));
202
+ var vs = aoff + voff;
203
+ if (vs + 66 <= rec.Length)
204
+ {
205
+ var parent = BitConverter.ToUInt64(rec.Slice(vs, 8));
206
+ var nlen = rec[vs + 64];
207
+ var ns = rec[vs + 65];
208
+ if (vs + 66 + nlen * 2 <= rec.Length && ns != 2)
209
+ {
210
+ var pr = ns == 1 ? 0 : ns == 3 ? 1 : ns == 0 ? 2 : 3;
211
+ if (pr < bestNs)
212
+ {
213
+ bestNs = (byte)pr;
214
+ best = (vs + 66, nlen, parent);
215
+ if (pr == 0) break;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ aoff += alen;
221
+ }
222
+
223
+ if (best is { } b)
224
+ {
225
+ var arenaOff = (uint)outNames.Count;
226
+ var chars = MemoryMarshal.Cast<byte, char>(rec.Slice(b.pos, b.len * 2));
227
+ AppendChars(outNames, chars);
228
+ outRecords.Add(new CompactRecord { FileRef = fileRef, ParentRef = b.parent, NameOff = arenaOff, NameLen = (ushort)b.len, IsDir = isDir });
229
+ }
230
+ }
231
+
232
+ private static void AppendChars(List<char> target, ReadOnlySpan<char> source)
233
+ {
234
+ target.Capacity = Math.Max(target.Capacity, target.Count + source.Length);
235
+ for (var i = 0; i < source.Length; i++) target.Add(source[i]);
236
+ }
237
+
238
+ public void Dispose() => _handle.Dispose();
239
+ }
FastSeek.Core/NativeMethods.cs ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Runtime.InteropServices;
2
+ using Microsoft.Win32.SafeHandles;
3
+
4
+ namespace FastSeek.Core.Mft;
5
+
6
+ internal static class NativeMethods
7
+ {
8
+ internal const uint FILE_SHARE_READ = 0x00000001;
9
+ internal const uint FILE_SHARE_WRITE = 0x00000002;
10
+ internal const uint FILE_SHARE_DELETE = 0x00000004;
11
+ internal const uint OPEN_EXISTING = 3;
12
+ internal const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
13
+ internal const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
14
+ internal const uint GENERIC_READ = 0x80000000;
15
+ internal const uint FILE_BEGIN = 0;
16
+
17
+ internal const uint FSCTL_ENUM_USN_DATA = 0x000900b3;
18
+ internal const uint FSCTL_QUERY_USN_JOURNAL = 0x000900f4;
19
+ internal const uint FSCTL_READ_USN_JOURNAL = 0x000900bb;
20
+
21
+ internal const uint USN_REASON_FILE_CREATE = 0x00000100;
22
+ internal const uint USN_REASON_FILE_DELETE = 0x00000200;
23
+ internal const uint USN_REASON_RENAME_OLD_NAME = 0x00001000;
24
+ internal const uint USN_REASON_RENAME_NEW_NAME = 0x00002000;
25
+
26
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
27
+ internal static extern uint GetLogicalDriveStrings(uint nBufferLength, [Out] char[] lpBuffer);
28
+
29
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
30
+ [return: MarshalAs(UnmanagedType.Bool)]
31
+ internal static extern bool GetVolumeInformation(
32
+ string lpRootPathName,
33
+ System.Text.StringBuilder? lpVolumeNameBuffer,
34
+ uint nVolumeNameSize,
35
+ out uint lpVolumeSerialNumber,
36
+ out uint lpMaximumComponentLength,
37
+ out uint lpFileSystemFlags,
38
+ [Out] char[] lpFileSystemNameBuffer,
39
+ uint nFileSystemNameSize);
40
+
41
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
42
+ internal static extern SafeFileHandle CreateFile(
43
+ string lpFileName,
44
+ uint dwDesiredAccess,
45
+ uint dwShareMode,
46
+ IntPtr lpSecurityAttributes,
47
+ uint dwCreationDisposition,
48
+ uint dwFlagsAndAttributes,
49
+ IntPtr hTemplateFile);
50
+
51
+ [DllImport("kernel32.dll", SetLastError = true)]
52
+ [return: MarshalAs(UnmanagedType.Bool)]
53
+ internal static extern bool ReadFile(
54
+ SafeFileHandle hFile,
55
+ [Out] byte[] lpBuffer,
56
+ uint nNumberOfBytesToRead,
57
+ out uint lpNumberOfBytesRead,
58
+ IntPtr lpOverlapped);
59
+
60
+ [DllImport("kernel32.dll", SetLastError = true)]
61
+ [return: MarshalAs(UnmanagedType.Bool)]
62
+ internal static extern bool SetFilePointerEx(SafeFileHandle hFile, long liDistanceToMove, out long lpNewFilePointer, uint dwMoveMethod);
63
+
64
+ [DllImport("kernel32.dll", SetLastError = true)]
65
+ [return: MarshalAs(UnmanagedType.Bool)]
66
+ internal static extern bool DeviceIoControl(
67
+ SafeFileHandle hDevice,
68
+ uint dwIoControlCode,
69
+ IntPtr lpInBuffer,
70
+ uint nInBufferSize,
71
+ [Out] byte[] lpOutBuffer,
72
+ uint nOutBufferSize,
73
+ out uint lpBytesReturned,
74
+ IntPtr lpOverlapped);
75
+
76
+ [DllImport("kernel32.dll", SetLastError = true)]
77
+ [return: MarshalAs(UnmanagedType.Bool)]
78
+ internal static extern bool DeviceIoControl(
79
+ SafeFileHandle hDevice,
80
+ uint dwIoControlCode,
81
+ IntPtr lpInBuffer,
82
+ uint nInBufferSize,
83
+ IntPtr lpOutBuffer,
84
+ uint nOutBufferSize,
85
+ out uint lpBytesReturned,
86
+ IntPtr lpOverlapped);
87
+ }
88
+
89
+ [StructLayout(LayoutKind.Sequential)]
90
+ internal struct MftEnumDataV0
91
+ {
92
+ public ulong StartFileReferenceNumber;
93
+ public long LowUsn;
94
+ public long HighUsn;
95
+ }
96
+
97
+ [StructLayout(LayoutKind.Sequential)]
98
+ internal struct UsnJournalDataV0
99
+ {
100
+ public ulong UsnJournalID;
101
+ public long FirstUsn;
102
+ public long NextUsn;
103
+ public long LowestValidUsn;
104
+ public long MaxUsn;
105
+ public ulong MaximumSize;
106
+ public ulong AllocationDelta;
107
+ }
108
+
109
+ [StructLayout(LayoutKind.Sequential)]
110
+ internal struct ReadUsnJournalDataV0
111
+ {
112
+ public long StartUsn;
113
+ public uint ReasonMask;
114
+ public uint ReturnOnlyOnClose;
115
+ public ulong Timeout;
116
+ public ulong BytesToWaitFor;
117
+ public ulong UsnJournalID;
118
+ }
119
+
120
+ [StructLayout(LayoutKind.Sequential)]
121
+ internal struct UsnRecordV2
122
+ {
123
+ public uint RecordLength;
124
+ public ushort MajorVersion;
125
+ public ushort MinorVersion;
126
+ public ulong FileReferenceNumber;
127
+ public ulong ParentFileReferenceNumber;
128
+ public long Usn;
129
+ public long TimeStamp;
130
+ public uint Reason;
131
+ public uint SourceInfo;
132
+ public uint SecurityId;
133
+ public uint FileAttributes;
134
+ public ushort FileNameLength;
135
+ public ushort FileNameOffset;
136
+ }
FastSeek.Core/SearchEngine.cs ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using FastSeek.Core.Index;
2
+
3
+ namespace FastSeek.Core.Search;
4
+
5
+ public sealed class SearchResult
6
+ {
7
+ public required string FullPath { get; init; }
8
+ public required string Name { get; init; }
9
+ public byte Rank { get; init; }
10
+ public bool IsDir { get; init; }
11
+ }
12
+
13
+ public static class SearchEngine
14
+ {
15
+ private static readonly HashSet<string> AppExtensions = new(StringComparer.OrdinalIgnoreCase) { "exe", "lnk", "msi", "appx", "msix" };
16
+ private static readonly string[] AppPathMarkers = ["\\program files\\", "\\program files (x86)\\", "\\start menu\\", "\\desktop\\", "\\appdata\\"];
17
+
18
+ public static List<SearchResult> Search(IndexStore store, string query, int limit, bool caseSensitive, IReadOnlyList<string> excludedDirs)
19
+ {
20
+ if (string.IsNullOrEmpty(query)) return [];
21
+ var q = caseSensitive ? query : query.ToLowerInvariant();
22
+
23
+ var bag = new System.Collections.Concurrent.ConcurrentBag<(int idx, byte rank)>();
24
+ Parallel.For(0, store.Entries.Count, i =>
25
+ {
26
+ var e = store.Entries[i];
27
+ var nameCmp = caseSensitive ? e.Name : e.NameLower;
28
+ byte rank;
29
+ if (nameCmp == q) rank = 1;
30
+ else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2;
31
+ else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3;
32
+ else return;
33
+ bag.Add((i, rank));
34
+ });
35
+
36
+ var candidates = bag.ToList();
37
+ candidates.Sort(static (a, b) => a.rank.CompareTo(b.rank));
38
+ if (candidates.Count > Math.Max(limit * 5, 1000)) candidates.RemoveRange(Math.Max(limit * 5, 1000), candidates.Count - Math.Max(limit * 5, 1000));
39
+
40
+ var results = new List<SearchResult>(limit);
41
+ foreach (var (idx, baseRank) in candidates)
42
+ {
43
+ var entry = store.Entries[idx];
44
+ var path = BuildPath(entry.FileRef, store);
45
+ if (excludedDirs.Count > 0)
46
+ {
47
+ var lowerPath = path.ToLowerInvariant();
48
+ var excluded = false;
49
+ for (var i = 0; i < excludedDirs.Count; i++)
50
+ {
51
+ if (lowerPath.StartsWith(excludedDirs[i], StringComparison.Ordinal)) { excluded = true; break; }
52
+ }
53
+ if (excluded) continue;
54
+ }
55
+
56
+ var rank = baseRank;
57
+ if (baseRank <= 2)
58
+ {
59
+ var extIdx = entry.NameLower.LastIndexOf('.');
60
+ if (extIdx >= 0 && extIdx + 1 < entry.NameLower.Length)
61
+ {
62
+ var ext = entry.NameLower[(extIdx + 1)..];
63
+ if (AppExtensions.Contains(ext))
64
+ {
65
+ var pathLower = path.ToLowerInvariant();
66
+ for (var i = 0; i < AppPathMarkers.Length; i++)
67
+ {
68
+ if (pathLower.Contains(AppPathMarkers[i], StringComparison.Ordinal)) { rank = 0; break; }
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ results.Add(new SearchResult { FullPath = path, Name = entry.Name, Rank = rank, IsDir = entry.IsDir });
75
+ }
76
+
77
+ results.Sort(static (a, b) => a.Rank.CompareTo(b.Rank));
78
+ if (results.Count > limit) results.RemoveRange(limit, results.Count - limit);
79
+ return results;
80
+ }
81
+
82
+ public static string BuildPath(ulong fileRef, IndexStore store)
83
+ {
84
+ var components = new List<string>(16);
85
+ var current = fileRef;
86
+ for (var i = 0; i < 64; i++)
87
+ {
88
+ var idx = store.LookupIdx(current);
89
+ if (idx is null) break;
90
+ var e = store.Entries[idx.Value];
91
+ components.Add(e.Name);
92
+ if (e.ParentRef == current) break;
93
+ current = e.ParentRef;
94
+ }
95
+
96
+ components.Reverse();
97
+ if (components.Count == 0) return store.DriveRoot;
98
+
99
+ var path = store.DriveRoot;
100
+ for (var i = 0; i < components.Count; i++) path = Path.Combine(path, components[i]);
101
+ return path;
102
+ }
103
+ }
FastSeek.Core/Types.cs ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Runtime.InteropServices;
2
+
3
+ namespace FastSeek.Core.Mft;
4
+
5
+ public enum FileKind
6
+ {
7
+ File,
8
+ Directory
9
+ }
10
+
11
+ public sealed class FileRecord
12
+ {
13
+ public ulong FileRef { get; init; }
14
+ public ulong ParentRef { get; init; }
15
+ public required string Name { get; init; }
16
+ public FileKind Kind { get; init; }
17
+ }
18
+
19
+ public sealed class NtfsDrive
20
+ {
21
+ public required char Letter { get; init; }
22
+ public required string Root { get; init; }
23
+ public required string DevicePath { get; init; }
24
+ }
25
+
26
+ public abstract record IndexEvent
27
+ {
28
+ public sealed record Created(FileRecord Record) : IndexEvent;
29
+ public sealed record Deleted(ulong FileRef) : IndexEvent;
30
+ public sealed record Renamed(ulong OldRef, FileRecord NewRecord) : IndexEvent;
31
+ public sealed record Moved(ulong FileRef, ulong NewParentRef, string Name, FileKind Kind) : IndexEvent;
32
+ }
33
+
34
+ public sealed class JournalCheckpoint
35
+ {
36
+ public long NextUsn { get; set; }
37
+ public ulong JournalId { get; set; }
38
+ public char DriveLetter { get; set; }
39
+ }
40
+
41
+ [StructLayout(LayoutKind.Sequential)]
42
+ internal struct CompactRecord
43
+ {
44
+ public ulong FileRef;
45
+ public ulong ParentRef;
46
+ public uint NameOff;
47
+ public ushort NameLen;
48
+ [MarshalAs(UnmanagedType.U1)]
49
+ public bool IsDir;
50
+ }
51
+
52
+ internal sealed class ScanResult
53
+ {
54
+ public required List<CompactRecord> Records { get; init; }
55
+ public required List<char> NameData { get; init; }
56
+ }
FastSeek.Core/UsnWatcher.cs ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Collections.Concurrent;
2
+ using System.Runtime.InteropServices;
3
+ using Microsoft.Win32.SafeHandles;
4
+
5
+ namespace FastSeek.Core.Mft;
6
+
7
+ public sealed class UsnWatcher : IDisposable
8
+ {
9
+ private const int BufferSize = 64 * 1024;
10
+ private readonly SafeFileHandle _handle;
11
+ private readonly NtfsDrive _drive;
12
+ private readonly BlockingCollection<IndexEvent> _sender;
13
+
14
+ public long NextUsn { get; private set; }
15
+ public ulong JournalId { get; private set; }
16
+
17
+ private UsnWatcher(SafeFileHandle handle, NtfsDrive drive, BlockingCollection<IndexEvent> sender, long nextUsn, ulong journalId)
18
+ {
19
+ _handle = handle;
20
+ _drive = drive;
21
+ _sender = sender;
22
+ NextUsn = nextUsn;
23
+ JournalId = journalId;
24
+ }
25
+
26
+ public static UsnWatcher New(NtfsDrive drive, BlockingCollection<IndexEvent> sender) => NewFrom(drive, sender, null);
27
+
28
+ public static UsnWatcher NewFrom(NtfsDrive drive, BlockingCollection<IndexEvent> sender, JournalCheckpoint? checkpoint)
29
+ {
30
+ var h = NativeMethods.CreateFile(drive.DevicePath, 0, NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE, IntPtr.Zero, NativeMethods.OPEN_EXISTING, NativeMethods.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
31
+ if (h.IsInvalid) throw new IOException("USN open failed");
32
+
33
+ var outPtr = Marshal.AllocHGlobal(Marshal.SizeOf<UsnJournalDataV0>());
34
+ try
35
+ {
36
+ if (!NativeMethods.DeviceIoControl(h, NativeMethods.FSCTL_QUERY_USN_JOURNAL, IntPtr.Zero, 0, outPtr, (uint)Marshal.SizeOf<UsnJournalDataV0>(), out _, IntPtr.Zero))
37
+ throw new IOException("USN query failed");
38
+
39
+ var data = Marshal.PtrToStructure<UsnJournalDataV0>(outPtr);
40
+ long nextUsn;
41
+ if (checkpoint is not null)
42
+ {
43
+ if (checkpoint.JournalId != data.UsnJournalID || checkpoint.NextUsn < data.FirstUsn || checkpoint.NextUsn > data.NextUsn)
44
+ throw new IOException("Journal mismatch - rescan needed");
45
+ nextUsn = checkpoint.NextUsn;
46
+ }
47
+ else nextUsn = data.NextUsn;
48
+
49
+ return new UsnWatcher(h, drive, sender, nextUsn, data.UsnJournalID);
50
+ }
51
+ finally { Marshal.FreeHGlobal(outPtr); }
52
+ }
53
+
54
+ public JournalCheckpoint Checkpoint() => new() { NextUsn = NextUsn, JournalId = JournalId, DriveLetter = _drive.Letter };
55
+
56
+ public void RunShared(List<JournalCheckpoint> checkpoints, object checkpointLock, CancellationToken token)
57
+ {
58
+ var buffer = new byte[BufferSize];
59
+ while (!token.IsCancellationRequested)
60
+ {
61
+ Thread.Sleep(500);
62
+ Poll(buffer);
63
+ lock (checkpointLock)
64
+ {
65
+ checkpoints.RemoveAll(c => c.DriveLetter == _drive.Letter);
66
+ checkpoints.Add(Checkpoint());
67
+ }
68
+ }
69
+ }
70
+
71
+ public int Drain()
72
+ {
73
+ var buffer = new byte[BufferSize];
74
+ var count = 0;
75
+ while (true)
76
+ {
77
+ var before = NextUsn;
78
+ Poll(buffer);
79
+ if (NextUsn == before) break;
80
+ count++;
81
+ }
82
+ return count;
83
+ }
84
+
85
+ private void Poll(byte[] buffer)
86
+ {
87
+ var read = new ReadUsnJournalDataV0
88
+ {
89
+ StartUsn = NextUsn,
90
+ ReasonMask = NativeMethods.USN_REASON_FILE_CREATE | NativeMethods.USN_REASON_FILE_DELETE | NativeMethods.USN_REASON_RENAME_NEW_NAME | NativeMethods.USN_REASON_RENAME_OLD_NAME,
91
+ ReturnOnlyOnClose = 0,
92
+ Timeout = 0,
93
+ BytesToWaitFor = 0,
94
+ UsnJournalID = JournalId,
95
+ };
96
+
97
+ var inPtr = Marshal.AllocHGlobal(Marshal.SizeOf<ReadUsnJournalDataV0>());
98
+ try
99
+ {
100
+ Marshal.StructureToPtr(read, inPtr, false);
101
+ if (!NativeMethods.DeviceIoControl(_handle, NativeMethods.FSCTL_READ_USN_JOURNAL, inPtr, (uint)Marshal.SizeOf<ReadUsnJournalDataV0>(), buffer, (uint)buffer.Length, out var returned, IntPtr.Zero) || returned <= 8) return;
102
+ NextUsn = BitConverter.ToInt64(buffer, 0);
103
+
104
+ var offset = 8;
105
+ while (offset + Marshal.SizeOf<UsnRecordV2>() <= returned)
106
+ {
107
+ var rec = MemoryMarshal.Read<UsnRecordV2>(new ReadOnlySpan<byte>(buffer, offset, Marshal.SizeOf<UsnRecordV2>()));
108
+ if (rec.RecordLength == 0) break;
109
+ ProcessRecord(rec, buffer, offset);
110
+ offset += (int)rec.RecordLength;
111
+ }
112
+ }
113
+ finally { Marshal.FreeHGlobal(inPtr); }
114
+ }
115
+
116
+ private void ProcessRecord(UsnRecordV2 record, byte[] buffer, int offset)
117
+ {
118
+ var nameLen = record.FileNameLength / 2;
119
+ var nameStart = offset + record.FileNameOffset;
120
+ var name = new string(MemoryMarshal.Cast<byte, char>(new ReadOnlySpan<byte>(buffer, nameStart, nameLen * 2)));
121
+ var isDir = (record.FileAttributes & 0x10) != 0;
122
+ var kind = isDir ? FileKind.Directory : FileKind.File;
123
+
124
+ if ((record.Reason & NativeMethods.USN_REASON_FILE_DELETE) != 0) { _sender.Add(new IndexEvent.Deleted(record.FileReferenceNumber)); return; }
125
+ if ((record.Reason & NativeMethods.USN_REASON_RENAME_NEW_NAME) != 0) { _sender.Add(new IndexEvent.Moved(record.FileReferenceNumber, record.ParentFileReferenceNumber, name, kind)); return; }
126
+ if ((record.Reason & NativeMethods.USN_REASON_FILE_CREATE) != 0) _sender.Add(new IndexEvent.Created(new FileRecord { FileRef = record.FileReferenceNumber, ParentRef = record.ParentFileReferenceNumber, Name = name, Kind = kind }));
127
+ }
128
+
129
+ public void Dispose() => _handle.Dispose();
130
+ }
FastSeek.WinUI/App.xaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Application
2
+ x:Class="FastSeek.WinUI.App"
3
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
5
+ <Application.Resources>
6
+ <ResourceDictionary>
7
+ <Color x:Key="Bg">#161A1D</Color>
8
+ <Color x:Key="Panel">#20262B</Color>
9
+ <Color x:Key="Accent">#4FC3F7</Color>
10
+ <SolidColorBrush x:Key="BgBrush" Color="{StaticResource Bg}" />
11
+ <SolidColorBrush x:Key="PanelBrush" Color="{StaticResource Panel}" />
12
+ <SolidColorBrush x:Key="AccentBrush" Color="{StaticResource Accent}" />
13
+ </ResourceDictionary>
14
+ </Application.Resources>
15
+ </Application>
FastSeek.WinUI/App.xaml.cs ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.UI.Xaml;
2
+
3
+ namespace FastSeek.WinUI;
4
+
5
+ public partial class App : Application
6
+ {
7
+ private Window? _window;
8
+
9
+ public App()
10
+ {
11
+ this.InitializeComponent();
12
+ }
13
+
14
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
15
+ {
16
+ _window = new MainWindow();
17
+ _window.Activate();
18
+ }
19
+ }
FastSeek.WinUI/FastSeek.WinUI.csproj ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>WinExe</OutputType>
4
+ <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
5
+ <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
6
+ <RootNamespace>FastSeek.WinUI</RootNamespace>
7
+ <ApplicationManifest>app.manifest</ApplicationManifest>
8
+ <Platforms>x64</Platforms>
9
+ <RuntimeIdentifier>win-x64</RuntimeIdentifier>
10
+ <UseWinUI>true</UseWinUI>
11
+ <Nullable>enable</Nullable>
12
+ <ImplicitUsings>enable</ImplicitUsings>
13
+
14
+ <WindowsPackageType>None</WindowsPackageType>
15
+ <AppxPackage>false</AppxPackage>
16
+ <EnableMsixTooling>false</EnableMsixTooling>
17
+ <WindowsAppSdkSelfContained>true</WindowsAppSdkSelfContained>
18
+ <GenerateAppxPackageOnBuild>false</GenerateAppxPackageOnBuild>
19
+ <EnablePreviewMsixTooling>false</EnablePreviewMsixTooling>
20
+ </PropertyGroup>
21
+ <ItemGroup>
22
+ <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
23
+ </ItemGroup>
24
+ <ItemGroup>
25
+ <ProjectReference Include="..\FastSeek.Core\FastSeek.Core.csproj" />
26
+ </ItemGroup>
27
+ </Project>
FastSeek.WinUI/MainWindow.xaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <Window
2
+ x:Class="FastSeek.WinUI.MainWindow"
3
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
7
+ mc:Ignorable="d"
8
+ Title="FastSeek">
9
+ <Grid Background="{StaticResource BgBrush}">
10
+ <Border Width="860" CornerRadius="18" Background="{StaticResource PanelBrush}" BorderBrush="#2F3B44" BorderThickness="1" Padding="14" HorizontalAlignment="Center" VerticalAlignment="Center">
11
+ <Grid RowDefinitions="Auto,*">
12
+ <AutoSuggestBox x:Name="SearchBox" PlaceholderText="Search files and folders..." QueryIcon="Find" TextChanged="SearchBox_TextChanged" KeyDown="SearchBox_KeyDown" FontSize="20" Background="#14191D" BorderBrush="#2A333A" Foreground="White" />
13
+ <ListView x:Name="ResultsList" Grid.Row="1" Margin="0,12,0,0" IsItemClickEnabled="True" ItemClick="ResultsList_ItemClick" SelectionMode="Single" Background="Transparent" Foreground="White">
14
+ <ListView.ItemTemplate>
15
+ <DataTemplate>
16
+ <Grid Padding="8,6">
17
+ <Grid.ColumnDefinitions>
18
+ <ColumnDefinition Width="58"/>
19
+ <ColumnDefinition Width="*"/>
20
+ </Grid.ColumnDefinitions>
21
+ <Border Grid.Column="0" Background="#132028" CornerRadius="8" Padding="8,3">
22
+ <TextBlock Text="{Binding Kind}" Foreground="{StaticResource AccentBrush}" FontWeight="SemiBold"/>
23
+ </Border>
24
+ <StackPanel Grid.Column="1" Margin="10,0,0,0">
25
+ <TextBlock Text="{Binding Name}" FontSize="15" TextTrimming="CharacterEllipsis"/>
26
+ <TextBlock Text="{Binding FullPath}" FontSize="12" Foreground="#B9C2CC" TextTrimming="CharacterEllipsis"/>
27
+ </StackPanel>
28
+ </Grid>
29
+ </DataTemplate>
30
+ </ListView.ItemTemplate>
31
+ </ListView>
32
+ </Grid>
33
+ </Border>
34
+ </Grid>
35
+ </Window>
FastSeek.WinUI/MainWindow.xaml.cs ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Collections.ObjectModel;
2
+ using System.Diagnostics;
3
+ using FastSeek.Core;
4
+ using FastSeek.Core.Search;
5
+ using Microsoft.UI.Input;
6
+ using Microsoft.UI.Windowing;
7
+ using Microsoft.UI.Xaml;
8
+ using Microsoft.UI.Xaml.Controls;
9
+ using Microsoft.UI.Xaml.Input;
10
+ using Windows.System;
11
+
12
+ namespace FastSeek.WinUI;
13
+
14
+ public sealed partial class MainWindow : Window
15
+ {
16
+ private readonly FastSeekEngine _engine = new();
17
+ private readonly ObservableCollection<ResultItem> _items = new();
18
+
19
+ public MainWindow()
20
+ {
21
+ this.InitializeComponent();
22
+ ResultsList.ItemsSource = _items;
23
+ ExtendsContentIntoTitleBar = true;
24
+ this.Closed += (_, _) => _engine.Dispose();
25
+ SetSpotlightSizing();
26
+ _ = InitializeEngineAsync();
27
+ }
28
+
29
+ private async Task InitializeEngineAsync()
30
+ {
31
+ SearchBox.PlaceholderText = "Indexing NTFS drives...";
32
+ try
33
+ {
34
+ await _engine.InitializeAsync();
35
+ SearchBox.PlaceholderText = "Search files and folders...";
36
+ }
37
+ catch (Exception ex)
38
+ {
39
+ SearchBox.PlaceholderText = "Initialization failed";
40
+ _items.Clear();
41
+ _items.Add(new ResultItem { Kind = "ERR", Name = "Could not initialize index", FullPath = ex.Message, Raw = null });
42
+ }
43
+ }
44
+
45
+ private void SetSpotlightSizing()
46
+ {
47
+ var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
48
+ var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
49
+ var appWindow = AppWindow.GetFromWindowId(id);
50
+ appWindow.Resize(new Windows.Graphics.SizeInt32(900, 560));
51
+ if (appWindow.Presenter is OverlappedPresenter p)
52
+ {
53
+ p.IsMaximizable = false;
54
+ p.IsMinimizable = true;
55
+ p.IsResizable = true;
56
+ }
57
+ }
58
+
59
+ private async void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
60
+ {
61
+ if (args.Reason != AutoSuggestionBoxTextChangeReason.UserInput) return;
62
+ var query = sender.Text;
63
+ var results = await Task.Run(() => _engine.Search(query, 50));
64
+ RenderResults(results);
65
+ }
66
+
67
+ private void RenderResults(List<SearchResult> results)
68
+ {
69
+ _items.Clear();
70
+ foreach (var r in results)
71
+ {
72
+ _items.Add(new ResultItem
73
+ {
74
+ Kind = r.IsDir ? "DIR" : "FILE",
75
+ Name = r.Name,
76
+ FullPath = r.FullPath,
77
+ Raw = r,
78
+ });
79
+ }
80
+ if (_items.Count > 0) ResultsList.SelectedIndex = 0;
81
+ }
82
+
83
+ private void SearchBox_KeyDown(object sender, KeyRoutedEventArgs e)
84
+ {
85
+ if (e.Key == VirtualKey.Down)
86
+ {
87
+ if (_items.Count == 0) return;
88
+ var i = Math.Min(ResultsList.SelectedIndex + 1, _items.Count - 1);
89
+ ResultsList.SelectedIndex = i;
90
+ ResultsList.ScrollIntoView(_items[i]);
91
+ e.Handled = true;
92
+ }
93
+ else if (e.Key == VirtualKey.Up)
94
+ {
95
+ if (_items.Count == 0) return;
96
+ var i = Math.Max(ResultsList.SelectedIndex - 1, 0);
97
+ ResultsList.SelectedIndex = i;
98
+ ResultsList.ScrollIntoView(_items[i]);
99
+ e.Handled = true;
100
+ }
101
+ else if (e.Key == VirtualKey.Enter)
102
+ {
103
+ OpenSelected();
104
+ e.Handled = true;
105
+ }
106
+ else if (e.Key == VirtualKey.Escape)
107
+ {
108
+ this.Close();
109
+ }
110
+ }
111
+
112
+ private void ResultsList_ItemClick(object sender, ItemClickEventArgs e) => OpenSelected();
113
+
114
+ private void OpenSelected()
115
+ {
116
+ if (ResultsList.SelectedItem is not ResultItem item) return;
117
+ try
118
+ {
119
+ Process.Start(new ProcessStartInfo
120
+ {
121
+ FileName = item.FullPath,
122
+ UseShellExecute = true,
123
+ });
124
+ }
125
+ catch
126
+ {
127
+ try
128
+ {
129
+ Process.Start(new ProcessStartInfo
130
+ {
131
+ FileName = "explorer.exe",
132
+ Arguments = $"/select,\"{item.FullPath}\"",
133
+ UseShellExecute = true,
134
+ });
135
+ }
136
+ catch { }
137
+ }
138
+ }
139
+
140
+ private sealed class ResultItem
141
+ {
142
+ public required string Kind { get; init; }
143
+ public required string Name { get; init; }
144
+ public required string FullPath { get; init; }
145
+ public SearchResult? Raw { get; init; }
146
+ }
147
+ }
FastSeek.WinUI/app.manifest ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
3
+ <assemblyIdentity version="1.0.0.0" name="FastSeek.WinUI.app"/>
4
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
5
+ <application>
6
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
7
+ </application>
8
+ </compatibility>
9
+ </assembly>
global.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "sdk": {
3
+ "version": "10.0.203"
4
+ }
5
+ }