File size: 4,263 Bytes
edcdb12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use rayon::prelude::*;
use crate::index::store::IndexStore;

const APP_EXTENSIONS: &[&str] = &["exe", "lnk", "msi", "appx", "msix"];
const APP_PATH_MARKERS: &[&str] = &[
    "\\program files\\", "\\program files (x86)\\",
    "\\start menu\\", "\\desktop\\", "\\appdata\\",
];

#[derive(Debug, Clone)]
pub struct SearchResult {
    pub full_path: std::path::PathBuf,
    pub name: String,
    pub rank: u8,
    pub is_dir: bool,
}

pub fn search(
    store: &IndexStore,
    query: &str,
    limit: usize,
    case_sensitive: bool,
    excluded_dirs: &[String],
) -> Vec<SearchResult> {
    if query.is_empty() {
        return Vec::new();
    }

    let q = if case_sensitive { query.to_string() } else { query.to_lowercase() };

    // ── Phase 1: lightweight name-only matching ──────────────────────────
    let entries = &store.entries;
    let name_lower_arena = &store.name_lower_arena;
    let name_arena = &store.name_arena;

    let mut candidates: Vec<(u32, u8)> = entries
        .par_iter()
        .enumerate()
        .filter_map(|(idx, entry)| {
            let name_cmp = if case_sensitive {
                unsafe { std::str::from_utf8_unchecked(&name_arena[entry.name_off as usize..(entry.name_off as usize + entry.name_len as usize)]) }
            } else {
                unsafe { std::str::from_utf8_unchecked(&name_lower_arena[entry.name_lower_off as usize..(entry.name_lower_off as usize + entry.name_lower_len as usize)]) }
            };

            let rank = if name_cmp == q { 1u8 }
                else if name_cmp.starts_with(&q) { 2 }
                else if name_cmp.contains(q.as_str()) { 3 }
                else { return None; };

            Some((idx as u32, rank))
        })
        .collect();

    // ── Phase 2: sort by rank, keep overshoot buffer ─────────────────
    candidates.sort_unstable_by_key(|&(_, rank)| rank);
    let overshoot = (limit * 5).max(1000);
    candidates.truncate(overshoot);

    // ── Phase 3: build paths + exclusions + app promotion ────────────
    let mut results: Vec<SearchResult> = Vec::with_capacity(limit);

    for &(idx, base_rank) in &candidates {
        let entry = &entries[idx as usize];
        let full_path = build_path(entry.file_ref, store);

        if !excluded_dirs.is_empty() {
            let path_lower = full_path.to_string_lossy().to_lowercase();
            if excluded_dirs.iter().any(|ex| path_lower.starts_with(ex.as_str())) {
                continue;
            }
        }

        let name_lower = store.name_lower(entry);
        let rank = if base_rank <= 2 {
            let ext_is_app = name_lower
                .rsplit('.')
                .next()
                .map(|e| APP_EXTENSIONS.contains(&e))
                .unwrap_or(false);
            if ext_is_app {
                let path_lower = full_path.to_string_lossy().to_lowercase();
                if APP_PATH_MARKERS.iter().any(|m| path_lower.contains(m)) { 0 } else { base_rank }
            } else {
                base_rank
            }
        } else {
            base_rank
        };

        results.push(SearchResult {
            full_path,
            name: store.name(entry).to_string(),
            rank,
            is_dir: entry.is_dir(),
        });
    }

    results.sort_unstable_by_key(|r| r.rank);
    results.truncate(limit);
    results
}

/// Iterative path builder β€” walks parent chain via sorted ref_lookup.
pub fn build_path(file_ref: u64, store: &IndexStore) -> std::path::PathBuf {
    let mut components: Vec<&str> = Vec::with_capacity(16);
    let mut current = file_ref;

    for _ in 0..64 {
        match store.lookup_idx(current) {
            Some(idx) => {
                let entry = &store.entries[idx as usize];
                components.push(store.name(entry));
                if entry.parent_ref == current {
                    break;
                }
                current = entry.parent_ref;
            }
            None => break,
        }
    }

    components.reverse();
    let mut path = std::path::PathBuf::from(&store.drive_root);
    for comp in components {
        path.push(comp);
    }
    path
}