File size: 4,054 Bytes
1c4658f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using FastSeek.Core.Index;

namespace FastSeek.Core.Search;

public sealed class SearchResult
{
    public required string FullPath { get; init; }
    public required string Name { get; init; }
    public byte Rank { get; init; }
    public bool IsDir { get; init; }
}

public static class SearchEngine
{
    private static readonly HashSet<string> AppExtensions = new(StringComparer.OrdinalIgnoreCase) { "exe", "lnk", "msi", "appx", "msix" };
    private static readonly string[] AppPathMarkers = ["\\program files\\", "\\program files (x86)\\", "\\start menu\\", "\\desktop\\", "\\appdata\\"];

    public static List<SearchResult> Search(IndexStore store, string query, int limit, bool caseSensitive, IReadOnlyList<string> excludedDirs)
    {
        if (string.IsNullOrEmpty(query)) return [];
        var q = caseSensitive ? query : query.ToLowerInvariant();

        var bag = new System.Collections.Concurrent.ConcurrentBag<(int idx, byte rank)>();
        Parallel.For(0, store.Entries.Count, i =>
        {
            var e = store.Entries[i];
            var nameCmp = caseSensitive ? e.Name : e.NameLower;
            byte rank;
            if (nameCmp == q) rank = 1;
            else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2;
            else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3;
            else return;
            bag.Add((i, rank));
        });

        var candidates = bag.ToList();
        candidates.Sort(static (a, b) => a.rank.CompareTo(b.rank));
        if (candidates.Count > Math.Max(limit * 5, 1000)) candidates.RemoveRange(Math.Max(limit * 5, 1000), candidates.Count - Math.Max(limit * 5, 1000));

        var results = new List<SearchResult>(limit);
        foreach (var (idx, baseRank) in candidates)
        {
            var entry = store.Entries[idx];
            var path = BuildPath(entry.FileRef, store);
            if (excludedDirs.Count > 0)
            {
                var lowerPath = path.ToLowerInvariant();
                var excluded = false;
                for (var i = 0; i < excludedDirs.Count; i++)
                {
                    if (lowerPath.StartsWith(excludedDirs[i], StringComparison.Ordinal)) { excluded = true; break; }
                }
                if (excluded) continue;
            }

            var rank = baseRank;
            if (baseRank <= 2)
            {
                var extIdx = entry.NameLower.LastIndexOf('.');
                if (extIdx >= 0 && extIdx + 1 < entry.NameLower.Length)
                {
                    var ext = entry.NameLower[(extIdx + 1)..];
                    if (AppExtensions.Contains(ext))
                    {
                        var pathLower = path.ToLowerInvariant();
                        for (var i = 0; i < AppPathMarkers.Length; i++)
                        {
                            if (pathLower.Contains(AppPathMarkers[i], StringComparison.Ordinal)) { rank = 0; break; }
                        }
                    }
                }
            }

            results.Add(new SearchResult { FullPath = path, Name = entry.Name, Rank = rank, IsDir = entry.IsDir });
        }

        results.Sort(static (a, b) => a.Rank.CompareTo(b.Rank));
        if (results.Count > limit) results.RemoveRange(limit, results.Count - limit);
        return results;
    }

    public static string BuildPath(ulong fileRef, IndexStore store)
    {
        var components = new List<string>(16);
        var current = fileRef;
        for (var i = 0; i < 64; i++)
        {
            var idx = store.LookupIdx(current);
            if (idx is null) break;
            var e = store.Entries[idx.Value];
            components.Add(e.Name);
            if (e.ParentRef == current) break;
            current = e.ParentRef;
        }

        components.Reverse();
        if (components.Count == 0) return store.DriveRoot;

        var path = store.DriveRoot;
        for (var i = 0; i < components.Count; i++) path = Path.Combine(path, components[i]);
        return path;
    }
}