use std::collections::HashSet;
#[cfg(target_os = "windows")]
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use crate::model::SearchItem;
const DEFAULT_INDEX_MAX_ITEMS_TOTAL: usize = 120_000;
const DEFAULT_INDEX_MAX_ITEMS_PER_ROOT: usize = 40_000;
const FILESYSTEM_DISCOVERY_SCHEMA_VERSION: &str = "2";
const TOP_LEVEL_EXCLUDED_DIR_NAMES: &[&str] = &[
"windows",
"program files",
"program files (x86)",
"$recycle.bin",
"system volume information",
"appdata",
];
const ANY_DEPTH_EXCLUDED_DIR_NAMES: &[&str] = &[
"node_modules",
".git",
".venv",
"venv",
"__pycache__",
"dist",
"build",
".gradle",
".m2",
".dropbox.cache",
".ssh",
];
const EXCLUDED_FILE_NAMES: &[&str] = &["pagefile.sys", "hiberfil.sys"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderError {
message: String,
}
impl ProviderError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl Display for ProviderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ProviderError {}
pub trait DiscoveryProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
fn discover(&self) -> Result<Vec<SearchItem>, ProviderError>;
fn change_stamp(&self) -> Option<String> {
None
}
}
pub struct AppProvider {
apps: Vec<SearchItem>,
}
impl AppProvider {
pub fn from_apps(apps: Vec<SearchItem>) -> Self {
Self { apps }
}
pub fn deterministic_fixture() -> Self {
Self {
apps: vec![
SearchItem::new(
"app-code",
"app",
"Visual Studio Code",
"C:\\Program Files\\Microsoft VS Code\\Code.exe",
),
SearchItem::new(
"app-term",
"app",
"Windows Terminal",
"C:\\Program Files\\WindowsApps\\Terminal.exe",
),
],
}
}
}
impl DiscoveryProvider for AppProvider {
fn provider_name(&self) -> &'static str {
"app"
}
fn discover(&self) -> Result<Vec<SearchItem>, ProviderError> {
Ok(self.apps.clone())
}
}
pub struct FileProvider {
files: Vec<SearchItem>,
}
impl FileProvider {
pub fn from_files(files: Vec<SearchItem>) -> Self {
Self { files }
}
pub fn deterministic_fixture() -> Self {
Self {
files: vec![
SearchItem::new(
"file-report",
"file",
"Q4_Report.xlsx",
"C:\\Users\\Admin\\Documents\\Q4_Report.xlsx",
),
SearchItem::new(
"file-notes",
"file",
"Meeting Notes.txt",
"C:\\Users\\Admin\\Documents\\Meeting Notes.txt",
),
],
}
}
}
impl DiscoveryProvider for FileProvider {
fn provider_name(&self) -> &'static str {
"file"
}
fn discover(&self) -> Result<Vec<SearchItem>, ProviderError> {
Ok(self.files.clone())
}
}
pub struct StartMenuAppDiscoveryProvider {
roots: Vec<PathBuf>,
}
impl Default for StartMenuAppDiscoveryProvider {
fn default() -> Self {
Self {
roots: default_start_menu_roots(),
}
}
}
impl StartMenuAppDiscoveryProvider {
pub fn with_roots(roots: Vec<PathBuf>) -> Self {
Self { roots }
}
}
impl DiscoveryProvider for StartMenuAppDiscoveryProvider {
fn provider_name(&self) -> &'static str {
"start-menu-apps"
}
fn discover(&self) -> Result<Vec<SearchItem>, ProviderError> {
#[cfg(not(target_os = "windows"))]
{
let _ = &self.roots;
Ok(Vec::new())
}
#[cfg(target_os = "windows")]
{
let uninstall_publishers = crate::uninstall_registry::publishers_by_display_name()
.unwrap_or_else(|error| {
crate::logging::warn(&format!(
"[nex] uninstall publisher map unavailable: {}",
error
));
HashMap::new()
});
let mut items = Vec::new();
for root in &self.roots {
items.extend(discover_start_menu_root(root, &uninstall_publishers)?);
}
if let Ok(system_apps) = discover_start_apps(&uninstall_publishers) {
items.extend(system_apps);
}
Ok(dedupe_apps_by_title(items))
}
}
fn change_stamp(&self) -> Option<String> {
const START_MENU_DISCOVERY_SCHEMA_VERSION: &str = "6";
Some(format!(
"v{START_MENU_DISCOVERY_SCHEMA_VERSION};{}",
roots_change_stamp(&self.roots)
))
}
}
pub struct FileSystemDiscoveryProvider {
roots: Vec<PathBuf>,
excluded_roots: Vec<PathBuf>,
max_depth: usize,
windows_search_enabled: bool,
windows_search_fallback_filesystem: bool,
show_files: bool,
show_folders: bool,
max_items_total: usize,
max_items_per_root: usize,
}
impl FileSystemDiscoveryProvider {
pub fn new(roots: Vec<PathBuf>, max_depth: usize, excluded_roots: Vec<PathBuf>) -> Self {
Self::with_options(roots, max_depth, excluded_roots, true, true, true, true)
}
pub fn with_windows_search_options(
roots: Vec<PathBuf>,
max_depth: usize,
excluded_roots: Vec<PathBuf>,
windows_search_enabled: bool,
windows_search_fallback_filesystem: bool,
) -> Self {
Self::with_options(
roots,
max_depth,
excluded_roots,
windows_search_enabled,
windows_search_fallback_filesystem,
true,
true,
)
}
pub fn with_options(
roots: Vec<PathBuf>,
max_depth: usize,
excluded_roots: Vec<PathBuf>,
windows_search_enabled: bool,
windows_search_fallback_filesystem: bool,
show_files: bool,
show_folders: bool,
) -> Self {
Self {
roots,
excluded_roots,
max_depth,
windows_search_enabled,
windows_search_fallback_filesystem,
show_files,
show_folders,
max_items_total: DEFAULT_INDEX_MAX_ITEMS_TOTAL,
max_items_per_root: DEFAULT_INDEX_MAX_ITEMS_PER_ROOT,
}
}
pub fn with_index_limits(mut self, max_items_total: usize, max_items_per_root: usize) -> Self {
let total = max_items_total.max(1);
let per_root = max_items_per_root.max(1).min(total);
self.max_items_total = total;
self.max_items_per_root = per_root;
self
}
}
impl DiscoveryProvider for FileSystemDiscoveryProvider {
fn provider_name(&self) -> &'static str {
"filesystem"
}
fn discover(&self) -> Result<Vec<SearchItem>, ProviderError> {
if !self.show_files && !self.show_folders {
return Ok(Vec::new());
}
#[cfg(target_os = "windows")]
if self.windows_search_enabled {
match discover_windows_search_items(
&self.roots,
&self.excluded_roots,
self.show_files,
self.show_folders,
self.max_items_total,
self.max_items_per_root,
) {
Ok(items) if !items.is_empty() => return Ok(items),
Ok(_) if !self.windows_search_fallback_filesystem => return Ok(Vec::new()),
Ok(_) => {}
Err(error) if !self.windows_search_fallback_filesystem => return Err(error),
Err(_) => {}
}
}
discover_filesystem_walk(
&self.roots,
&self.excluded_roots,
self.max_depth,
self.show_files,
self.show_folders,
self.max_items_total,
self.max_items_per_root,
)
}
fn change_stamp(&self) -> Option<String> {
let mut stamp = String::new();
stamp.push_str("schema=");
stamp.push_str(FILESYSTEM_DISCOVERY_SCHEMA_VERSION);
stamp.push(';');
stamp.push_str("roots=");
stamp.push_str(&roots_change_stamp(&self.roots));
stamp.push_str(";exclude=");
stamp.push_str(&roots_change_stamp(&self.excluded_roots));
stamp.push_str(";depth=");
stamp.push_str(&self.max_depth.to_string());
stamp.push_str(";windows_search=");
stamp.push_str(if self.windows_search_enabled {
"enabled"
} else {
"disabled"
});
stamp.push_str(";fallback=");
stamp.push_str(if self.windows_search_fallback_filesystem {
"filesystem"
} else {
"none"
});
stamp.push_str(";show_files=");
stamp.push_str(if self.show_files { "true" } else { "false" });
stamp.push_str(";show_folders=");
stamp.push_str(if self.show_folders { "true" } else { "false" });
stamp.push_str(";cap_total=");
stamp.push_str(&self.max_items_total.to_string());
stamp.push_str(";cap_per_root=");
stamp.push_str(&self.max_items_per_root.to_string());
Some(stamp)
}
}
fn discover_filesystem_walk(
roots: &[PathBuf],
excluded_roots: &[PathBuf],
max_depth: usize,
show_files: bool,
show_folders: bool,
max_items_total: usize,
max_items_per_root: usize,
) -> Result<Vec<SearchItem>, ProviderError> {
let mut out = Vec::new();
let exclusion_policy = DiscoveryExclusionPolicy::new(excluded_roots);
let total_budget = max_items_total.max(1);
let per_root_budget = max_items_per_root.max(1).min(total_budget);
let mut total_added = 0_usize;
let mut skipped_due_exclusion = 0_usize;
for root in roots {
if total_added >= total_budget {
break;
}
if !root.exists() {
continue;
}
if exclusion_policy.should_exclude_path_under_root(root, root) {
skipped_due_exclusion = skipped_due_exclusion.saturating_add(1);
continue;
}
let mut root_added = 0_usize;
for entry in walkdir::WalkDir::new(root)
.max_depth(max_depth)
.into_iter()
.filter_entry(|entry| {
let excluded = exclusion_policy.should_exclude_path_under_root(entry.path(), root);
if excluded && entry.path() != root {
skipped_due_exclusion = skipped_due_exclusion.saturating_add(1);
}
!excluded
})
.filter_map(Result::ok)
{
if total_added >= total_budget || root_added >= per_root_budget {
break;
}
let path = entry.path();
if path.is_dir() {
if !show_folders {
continue;
}
if path == root {
continue;
}
let folder_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
let id = format!("folder:{}", path.to_string_lossy());
out.push(SearchItem::new(
&id,
"folder",
&folder_name,
&path.to_string_lossy(),
));
total_added += 1;
root_added += 1;
continue;
}
if !path.is_file() {
continue;
}
if !show_files {
continue;
}
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
let id = format!("file:{}", path.to_string_lossy());
out.push(SearchItem::new(
&id,
"file",
&file_name,
&path.to_string_lossy(),
));
total_added += 1;
root_added += 1;
}
}
if total_added >= total_budget {
crate::logging::info(&format!(
"[nex] discovery_cap provider=filesystem total_cap={} reached=true",
total_budget
));
}
if skipped_due_exclusion > 0 {
crate::logging::info(&format!(
"[nex] discovery_exclusion provider=filesystem skipped={} policy_schema={}",
skipped_due_exclusion, FILESYSTEM_DISCOVERY_SCHEMA_VERSION
));
}
Ok(out)
}
#[derive(Debug, Clone)]
struct DiscoveryExclusionPolicy {
excluded_roots: Vec<String>,
top_level_dir_names: HashSet<&'static str>,
any_depth_dir_names: HashSet<&'static str>,
file_names: HashSet<&'static str>,
}
impl DiscoveryExclusionPolicy {
fn new(user_excluded_roots: &[PathBuf]) -> Self {
Self {
excluded_roots: effective_normalized_exclusion_roots(user_excluded_roots),
top_level_dir_names: TOP_LEVEL_EXCLUDED_DIR_NAMES.iter().copied().collect(),
any_depth_dir_names: ANY_DEPTH_EXCLUDED_DIR_NAMES.iter().copied().collect(),
file_names: EXCLUDED_FILE_NAMES.iter().copied().collect(),
}
}
fn should_exclude_path_under_root(&self, path: &Path, root: &Path) -> bool {
if is_path_under_any_excluded_root(path, &self.excluded_roots) {
return true;
}
let Ok(relative) = path.strip_prefix(root) else {
return false;
};
let components = relative
.components()
.filter_map(|component| match component {
std::path::Component::Normal(value) => {
Some(value.to_string_lossy().to_ascii_lowercase())
}
_ => None,
})
.collect::<Vec<_>>();
if components.is_empty() {
return false;
}
if self.top_level_dir_names.contains(components[0].as_str()) {
return true;
}
let is_dir = path.is_dir();
for (index, component) in components.iter().enumerate() {
let is_last = index + 1 == components.len();
if self.file_names.contains(component.as_str()) {
return true;
}
if self.any_depth_dir_names.contains(component.as_str()) && (!is_last || is_dir) {
return true;
}
}
false
}
}
fn effective_normalized_exclusion_roots(user_excluded_roots: &[PathBuf]) -> Vec<String> {
let mut roots = builtin_exclusion_roots();
roots.extend(user_excluded_roots.iter().cloned());
normalized_exclusion_roots(&roots)
}
fn builtin_exclusion_roots() -> Vec<PathBuf> {
#[cfg(target_os = "windows")]
{
let mut roots = Vec::new();
if let Ok(system_drive) = std::env::var("SystemDrive") {
let trimmed = system_drive.trim();
if !trimmed.is_empty() {
let drive_root = PathBuf::from(format!("{trimmed}\\"));
roots.push(drive_root.join("Windows"));
roots.push(drive_root.join("Program Files"));
roots.push(drive_root.join("Program Files (x86)"));
roots.push(drive_root.join("$Recycle.Bin"));
roots.push(drive_root.join("System Volume Information"));
roots.push(drive_root.join("pagefile.sys"));
roots.push(drive_root.join("hiberfil.sys"));
}
}
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
let local = PathBuf::from(local_app_data.trim());
if !local.as_os_str().is_empty() {
roots.push(local.join("Temp"));
roots.push(local.join("Microsoft").join("Windows").join("INetCache"));
}
}
if let Ok(app_data) = std::env::var("APPDATA") {
let roaming = PathBuf::from(app_data.trim());
if !roaming.as_os_str().is_empty() {
if let Some(parent) = roaming.parent().and_then(|path| path.parent()) {
roots.push(parent.join("AppData"));
roots.push(parent.join(".ssh"));
}
}
}
roots
}
#[cfg(not(target_os = "windows"))]
{
Vec::new()
}
}
fn roots_change_stamp(roots: &[PathBuf]) -> String {
let mut parts = Vec::with_capacity(roots.len());
for root in roots {
let normalized = normalize_root_for_stamp(root);
let (exists, modified_secs, child_count, child_latest_secs) = quick_path_fingerprint(root);
parts.push(format!(
"{normalized}:{exists}:{modified_secs}:{child_count}:{child_latest_secs}"
));
}
parts.join("|")
}
fn normalize_root_for_stamp(path: &Path) -> String {
path.to_string_lossy()
.replace('/', "\\")
.to_ascii_lowercase()
}
fn quick_path_fingerprint(path: &Path) -> (u8, u64, usize, u64) {
let Ok(meta) = std::fs::metadata(path) else {
return (0, 0, 0, 0);
};
let root_modified_secs = modified_secs(&meta);
let mut child_count = 0_usize;
let mut child_latest_secs = 0_u64;
if meta.is_dir() {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
child_count += 1;
if let Ok(child_meta) = entry.metadata() {
child_latest_secs = child_latest_secs.max(modified_secs(&child_meta));
}
}
}
}
(1, root_modified_secs, child_count, child_latest_secs)
}
fn modified_secs(meta: &std::fs::Metadata) -> u64 {
meta.modified()
.ok()
.and_then(|value| value.duration_since(UNIX_EPOCH).ok())
.map(|value| value.as_secs())
.unwrap_or(0)
}
fn normalized_exclusion_roots(excluded_roots: &[PathBuf]) -> Vec<String> {
excluded_roots
.iter()
.filter_map(|root| normalize_path_for_compare(root).filter(|v| !v.is_empty()))
.collect()
}
fn is_path_under_any_excluded_root(path: &Path, excluded_roots: &[String]) -> bool {
let Some(path_norm) = normalize_path_for_compare(path) else {
return false;
};
excluded_roots.iter().any(|root| {
path_norm == *root
|| (path_norm.starts_with(root) && path_norm[root.len()..].starts_with('\\'))
})
}
fn normalize_path_for_compare(path: &Path) -> Option<String> {
let mut value = path.to_string_lossy().replace('/', "\\");
while value.ends_with('\\') {
value.pop();
}
let value = value.trim().to_ascii_lowercase();
if value.is_empty() {
None
} else {
Some(value)
}
}
#[cfg(target_os = "windows")]
fn default_start_menu_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Ok(program_data) = std::env::var("ProgramData") {
roots.push(
PathBuf::from(program_data)
.join("Microsoft")
.join("Windows")
.join("Start Menu")
.join("Programs"),
);
}
if let Ok(app_data) = std::env::var("APPDATA") {
roots.push(
PathBuf::from(app_data)
.join("Microsoft")
.join("Windows")
.join("Start Menu")
.join("Programs"),
);
}
roots
}
#[cfg(not(target_os = "windows"))]
fn default_start_menu_roots() -> Vec<PathBuf> {
Vec::new()
}
#[cfg(target_os = "windows")]
fn discover_start_menu_root(
root: &Path,
uninstall_publishers: &HashMap<String, String>,
) -> Result<Vec<SearchItem>, ProviderError> {
if !root.exists() {
return Ok(Vec::new());
}
#[derive(Debug, Clone)]
struct StartMenuCandidate {
path: PathBuf,
title: String,
ext: String,
shortcut_target: Option<String>,
}
let mut candidates: Vec<StartMenuCandidate> = Vec::new();
for entry in walkdir::WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
{
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
let title = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
if ext != "lnk" && ext != "exe" {
continue;
}
let mut resolved_shortcut_target = None;
if ext == "lnk" {
resolved_shortcut_target = resolve_shortcut_target_for_discovery(path);
if let Some(shortcut_target) = resolved_shortcut_target.as_deref() {
if should_exclude_non_app_start_reference(title.as_str(), shortcut_target) {
continue;
}
}
}
if ext == "lnk" && !shortcut_has_launch_target(path) {
continue;
}
if is_documentation_like_start_entry_title(&title) {
continue;
}
candidates.push(StartMenuCandidate {
path: path.to_path_buf(),
title,
ext,
shortcut_target: resolved_shortcut_target,
});
}
let mut exe_paths = HashSet::new();
for candidate in &candidates {
if candidate.ext == "exe" {
let exe = normalize_shortcut_target_path(candidate.path.to_string_lossy().as_ref());
if !exe.is_empty() {
exe_paths.insert(exe);
}
continue;
}
if let Some(target) = candidate.shortcut_target.as_deref() {
let normalized_target = normalize_shortcut_target_path(target);
if looks_like_filesystem_path(normalized_target.as_str())
&& normalized_target.to_ascii_lowercase().ends_with(".exe")
{
exe_paths.insert(normalized_target);
}
}
}
let mut exe_paths_vec: Vec<String> = exe_paths.into_iter().collect();
exe_paths_vec.sort();
let exe_publishers = load_exe_company_names(&exe_paths_vec).unwrap_or_default();
let mut items = Vec::with_capacity(candidates.len());
for candidate in candidates {
let path_text = candidate.path.to_string_lossy().to_string();
let id = format!("app:{path_text}");
let mut subtitle = String::new();
if let Some(publisher) =
publisher_from_uninstall_map(candidate.title.as_str(), uninstall_publishers)
{
subtitle = publisher;
}
if subtitle.trim().is_empty() {
let exe_target = if candidate.ext == "exe" {
normalize_shortcut_target_path(path_text.as_str())
} else {
candidate
.shortcut_target
.as_deref()
.map(normalize_shortcut_target_path)
.unwrap_or_default()
};
if !exe_target.trim().is_empty() {
let exe_key = normalize_id_path(exe_target.as_str());
if let Some(exe_subtitle) = exe_publishers.get(&exe_key) {
subtitle = exe_subtitle.clone();
}
}
}
if subtitle.trim().is_empty() {
if let Some(fallback) = start_menu_entry_subtitle(
root,
candidate.path.as_path(),
candidate.shortcut_target.as_deref(),
) {
subtitle = fallback;
}
}
let mut item = SearchItem::new(&id, "app", &candidate.title, &path_text);
if let Some(clean_subtitle) = sanitize_publisher_label(subtitle.as_str()) {
item = item.with_subtitle(clean_subtitle.as_str());
}
items.push(item);
}
Ok(items)
}
#[cfg(target_os = "windows")]
fn discover_start_apps(
uninstall_publishers: &HashMap<String, String>,
) -> Result<Vec<SearchItem>, ProviderError> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let script = r#"
$ErrorActionPreference = 'Stop'
Get-StartApps | ForEach-Object {
$name = [string]$_.Name
$appId = [string]$_.AppID
if (-not [string]::IsNullOrWhiteSpace($name) -and -not [string]::IsNullOrWhiteSpace($appId)) {
"{0}`t{1}" -f $name.Trim(), $appId.Trim()
}
}
"#;
let mut command = Command::new("powershell.exe");
command
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-WindowStyle",
"Hidden",
"-Command",
script,
])
.creation_flags(CREATE_NO_WINDOW);
let output = command
.output()
.map_err(|error| ProviderError::new(format!("Get-StartApps invocation failed: {error}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(ProviderError::new(format!(
"Get-StartApps failed (status={}): {}",
output.status,
if stderr.is_empty() {
"no stderr"
} else {
stderr.as_str()
}
)));
}
let appx_publishers = load_appx_family_publishers().unwrap_or_default();
let mut items = Vec::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let Some((name, app_id)) = line.split_once('\t') else {
continue;
};
let title = name.trim();
let app_id = app_id.trim();
if title.is_empty() || app_id.is_empty() {
continue;
}
if should_exclude_non_app_start_reference(title, app_id) {
continue;
}
if is_documentation_like_start_entry_title(title) {
continue;
}
let path = format!("shell:AppsFolder\\{app_id}");
let id = format!("app:{}", normalize_id_path(&path));
let mut item = SearchItem::new(&id, "app", title, &path);
if let Some(subtitle) = publisher_from_uninstall_map(title, uninstall_publishers)
.or_else(|| start_app_subtitle_from_app_id(app_id, &appx_publishers))
.and_then(|raw| sanitize_publisher_label(raw.as_str()))
{
item = item.with_subtitle(subtitle.as_str());
}
items.push(item);
}
Ok(items)
}
#[cfg(target_os = "windows")]
fn load_appx_family_publishers() -> Result<HashMap<String, String>, ProviderError> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let script = r#"
$ErrorActionPreference = 'Stop'
Get-AppxPackage | ForEach-Object {
$family = [string]$_.PackageFamilyName
if ([string]::IsNullOrWhiteSpace($family)) { return }
$publisher = [string]$_.PublisherDisplayName
if ([string]::IsNullOrWhiteSpace($publisher)) {
$raw = [string]$_.Publisher
if (-not [string]::IsNullOrWhiteSpace($raw)) {
if ($raw -match 'CN=([^,]+)') { $publisher = $matches[1] } else { $publisher = $raw }
}
}
if (-not [string]::IsNullOrWhiteSpace($publisher)) {
"{0}`t{1}" -f $family.Trim(), $publisher.Trim()
}
}
"#;
let mut command = Command::new("powershell.exe");
command
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-WindowStyle",
"Hidden",
"-Command",
script,
])
.creation_flags(CREATE_NO_WINDOW);
let output = command.output().map_err(|error| {
ProviderError::new(format!("Get-AppxPackage invocation failed: {error}"))
})?;
if !output.status.success() {
return Ok(HashMap::new());
}
let mut out = HashMap::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let mut parts = line.splitn(2, '\t');
let Some(family_raw) = parts.next() else {
continue;
};
let Some(publisher_raw) = parts.next() else {
continue;
};
let family = family_raw.trim();
let publisher = publisher_raw.trim();
if family.is_empty() || publisher.is_empty() {
continue;
}
if let Some(clean_publisher) = sanitize_publisher_label(publisher) {
out.insert(family.to_ascii_lowercase(), clean_publisher);
}
}
Ok(out)
}
#[cfg(target_os = "windows")]
fn load_exe_company_names(exe_paths: &[String]) -> Result<HashMap<String, String>, ProviderError> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
if exe_paths.is_empty() {
return Ok(HashMap::new());
}
let joined_paths = exe_paths.join("\u{1f}");
if joined_paths.trim().is_empty() {
return Ok(HashMap::new());
}
let script = r#"
$ErrorActionPreference = 'Stop'
$separator = [char]0x1f
$paths = @()
if ($env:NEX_EXE_PATHS) { $paths = $env:NEX_EXE_PATHS -split $separator }
elseif ($env:SWIFTFIND_EXE_PATHS) { $paths = $env:SWIFTFIND_EXE_PATHS -split $separator }
foreach ($path in $paths) {
$candidate = [string]$path
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { continue }
try {
$publisher = [string][System.Diagnostics.FileVersionInfo]::GetVersionInfo($candidate).CompanyName
if (-not [string]::IsNullOrWhiteSpace($publisher)) {
"{0}`t{1}" -f $candidate, $publisher.Trim()
}
} catch {}
}
"#;
let mut command = Command::new("powershell.exe");
command
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-WindowStyle",
"Hidden",
"-Command",
script,
])
.env("NEX_EXE_PATHS", &joined_paths)
.env("SWIFTFIND_EXE_PATHS", joined_paths)
.creation_flags(CREATE_NO_WINDOW);
let output = command.output().map_err(|error| {
ProviderError::new(format!(
"exe publisher discovery invocation failed: {error}"
))
})?;
if !output.status.success() {
return Ok(HashMap::new());
}
let mut out = HashMap::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let mut parts = line.splitn(2, '\t');
let Some(path_raw) = parts.next() else {
continue;
};
let Some(publisher_raw) = parts.next() else {
continue;
};
let path = path_raw.trim();
let publisher = publisher_raw.trim();
if path.is_empty() || publisher.is_empty() {
continue;
}
if let Some(clean_publisher) = sanitize_publisher_label(publisher) {
out.insert(normalize_id_path(path), clean_publisher);
}
}
Ok(out)
}
#[cfg(target_os = "windows")]
fn dedupe_apps_by_title(items: Vec<SearchItem>) -> Vec<SearchItem> {
let mut by_title: HashMap<String, SearchItem> = HashMap::new();
for item in items {
let title_key = crate::model::normalize_for_search(item.title.trim());
let key = if title_key.is_empty() {
format!("path:{}", normalize_id_path(&item.path))
} else {
title_key
};
match by_title.get(&key) {
Some(existing) if app_quality_rank(existing) >= app_quality_rank(&item) => {}
_ => {
by_title.insert(key, item);
}
}
}
let mut out: Vec<SearchItem> = by_title.into_values().collect();
out.sort_by(|a, b| {
a.title
.to_ascii_lowercase()
.cmp(&b.title.to_ascii_lowercase())
});
out
}
#[cfg(target_os = "windows")]
fn app_quality_rank(item: &SearchItem) -> u8 {
let subtitle_bonus = subtitle_quality_score(item.subtitle.as_str());
let lowered = item.path.trim().to_ascii_lowercase();
if lowered.starts_with("shell:appsfolder\\") {
return 3 + subtitle_bonus;
}
if lowered.ends_with(".lnk") || lowered.ends_with(".exe") {
return 2 + subtitle_bonus;
}
1 + subtitle_bonus
}
#[cfg(target_os = "windows")]
fn subtitle_quality_score(subtitle: &str) -> u8 {
let trimmed = subtitle.trim();
if trimmed.is_empty() {
return 0;
}
let word_count = trimmed.split_whitespace().count();
if word_count >= 3 {
3
} else if word_count == 2 {
2
} else {
1
}
}
#[cfg(target_os = "windows")]
fn start_menu_entry_subtitle(
_root: &Path,
_entry_path: &Path,
shortcut_target: Option<&str>,
) -> Option<String> {
let shortcut_target = shortcut_target?;
let normalized_target = normalize_shortcut_target_path(shortcut_target);
if normalized_target.is_empty() || !looks_like_filesystem_path(normalized_target.as_str()) {
return None;
}
program_files_vendor_label(normalized_target.as_str())
}
#[cfg(target_os = "windows")]
fn program_files_vendor_label(target_path: &str) -> Option<String> {
let normalized = target_path.replace('/', "\\");
let lower = normalized.to_ascii_lowercase();
let markers = ["\\program files\\", "\\program files (x86)\\"];
for marker in markers {
let Some(start) = lower.find(marker) else {
continue;
};
let tail = &normalized[start + marker.len()..];
let vendor = tail.split('\\').next().unwrap_or("").trim();
if vendor.is_empty() {
continue;
}
let vendor_lower = vendor.to_ascii_lowercase();
if matches!(
vendor_lower.as_str(),
"windowsapps" | "common files" | "windows nt"
) {
continue;
}
return Some(vendor.to_string());
}
None
}
#[cfg(target_os = "windows")]
fn start_app_subtitle_from_app_id(
app_id: &str,
appx_publishers: &HashMap<String, String>,
) -> Option<String> {
let trimmed = app_id.trim();
if trimmed.is_empty() {
return None;
}
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("microsoft.autogenerated.") {
return None;
}
if let Some((package_name, _app_entry)) = trimmed.split_once('!') {
let family_key = package_name.trim().to_ascii_lowercase();
if let Some(label) = appx_publishers.get(&family_key) {
let cleaned = label.trim();
if !cleaned.is_empty() {
return Some(cleaned.to_string());
}
}
if let Some((publisher_hint, _package_tail)) = package_name.split_once('_') {
if let Some(publisher) = normalize_publisher_hint(publisher_hint) {
return Some(publisher);
}
}
}
None
}
#[cfg(target_os = "windows")]
fn publisher_from_uninstall_map(
title: &str,
uninstall_publishers: &HashMap<String, String>,
) -> Option<String> {
let key = crate::model::normalize_for_search(title);
if key.is_empty() {
return None;
}
uninstall_publishers
.get(&key)
.and_then(|publisher| sanitize_publisher_label(publisher.as_str()))
}
#[cfg(target_os = "windows")]
fn normalize_publisher_hint(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let head = trimmed.split('.').find(|part| !part.trim().is_empty())?;
let head = head.trim();
if head.is_empty() {
return None;
}
let lower = head.to_ascii_lowercase();
if lower.starts_with("microsoft") {
return Some("Microsoft".to_string());
}
sanitize_publisher_label(head)
}
#[cfg(target_os = "windows")]
fn sanitize_publisher_label(raw: &str) -> Option<String> {
let mut trimmed = raw.trim().trim_matches('"').trim().to_string();
if trimmed.is_empty() {
return None;
}
if trimmed.to_ascii_lowercase().starts_with("cn=") {
trimmed = trimmed
.get(3..)
.unwrap_or_default()
.split(',')
.next()
.unwrap_or("")
.trim()
.to_string();
if trimmed.is_empty() {
return None;
}
}
let collapsed = trimmed
.split_whitespace()
.filter(|part| !part.trim().is_empty())
.collect::<Vec<_>>()
.join(" ");
if collapsed.is_empty() {
return None;
}
if collapsed.contains('\\')
|| collapsed.contains('/')
|| collapsed.contains('{')
|| collapsed.contains('}')
|| collapsed.contains("://")
{
return None;
}
if looks_like_guid_token(collapsed.as_str())
|| looks_like_noisy_publisher_token(collapsed.as_str())
{
return None;
}
let lowered = collapsed.to_ascii_lowercase();
if lowered == "microsoftwindows" {
return Some("Microsoft".to_string());
}
Some(collapsed)
}
#[cfg(target_os = "windows")]
fn looks_like_guid_token(value: &str) -> bool {
let token = value
.trim()
.trim_matches('{')
.trim_matches('}')
.trim()
.to_ascii_lowercase();
let parts: Vec<&str> = token.split('-').collect();
if parts.len() != 5 {
return false;
}
let expected = [8, 4, 4, 4, 12];
for (part, expected_len) in parts.iter().zip(expected.iter()) {
if part.len() != *expected_len || !part.chars().all(|ch| ch.is_ascii_hexdigit()) {
return false;
}
}
true
}
#[cfg(target_os = "windows")]
fn looks_like_noisy_publisher_token(value: &str) -> bool {
let compact = value
.chars()
.filter(|ch| !ch.is_whitespace())
.collect::<String>();
if compact.is_empty() {
return true;
}
let lower = compact.to_ascii_lowercase();
if lower.starts_with("microsoft.autogenerated") || lower.contains("autogenerated") {
return true;
}
if compact.contains('_') {
return true;
}
if !value.contains(' ') && compact.contains('.') {
return true;
}
let letters = compact
.chars()
.filter(|ch| ch.is_ascii_alphabetic())
.count();
let digits = compact.chars().filter(|ch| ch.is_ascii_digit()).count();
if letters == 0 {
return true;
}
if !value.contains(' ') {
if digits > 0 && digits * 2 >= letters {
return true;
}
if compact.len() >= 14 && digits >= 3 {
return true;
}
}
false
}
#[cfg(target_os = "windows")]
fn normalize_id_path(path: &str) -> String {
path.trim().replace('/', "\\").to_ascii_lowercase()
}
#[cfg(target_os = "windows")]
fn shortcut_has_launch_target(shortcut_path: &Path) -> bool {
use windows_sys::Win32::UI::Shell::HlinkResolveShortcutToString;
let wide_shortcut = to_wide(shortcut_path.to_string_lossy().as_ref());
let mut target: windows_sys::core::PWSTR = std::ptr::null_mut();
let mut location: windows_sys::core::PWSTR = std::ptr::null_mut();
let hr =
unsafe { HlinkResolveShortcutToString(wide_shortcut.as_ptr(), &mut target, &mut location) };
if hr < 0 {
return false;
}
let resolved_target = pwstr_to_string_and_free(target);
let resolved_location = pwstr_to_string_and_free(location);
if shortcut_resolves_to_web_target(&resolved_target)
|| shortcut_resolves_to_web_target(&resolved_location)
{
return false;
}
let resolved_target = normalize_shortcut_target_path(resolved_target.as_str());
if resolved_target.is_empty() {
return false;
}
if looks_like_filesystem_path(resolved_target.as_str()) {
return Path::new(resolved_target.as_str()).exists();
}
true
}
#[cfg(target_os = "windows")]
fn resolve_shortcut_target_for_discovery(shortcut_path: &Path) -> Option<String> {
use windows_sys::Win32::UI::Shell::HlinkResolveShortcutToString;
let wide_shortcut = to_wide(shortcut_path.to_string_lossy().as_ref());
let mut target: windows_sys::core::PWSTR = std::ptr::null_mut();
let mut location: windows_sys::core::PWSTR = std::ptr::null_mut();
let hr =
unsafe { HlinkResolveShortcutToString(wide_shortcut.as_ptr(), &mut target, &mut location) };
if hr < 0 {
return None;
}
let resolved_target = pwstr_to_string_and_free(target);
let resolved_location = pwstr_to_string_and_free(location);
let preferred = normalize_shortcut_target_path(resolved_target.as_str());
if !preferred.is_empty() {
return Some(preferred);
}
let fallback = normalize_shortcut_target_path(resolved_location.as_str());
if fallback.is_empty() {
None
} else {
Some(fallback)
}
}
#[cfg(target_os = "windows")]
fn should_exclude_non_app_start_reference(title: &str, reference: &str) -> bool {
if is_excluded_windows_kits_shortcut_reference(reference) {
return true;
}
if shortcut_resolves_to_web_target(reference) {
return true;
}
if has_non_app_document_extension(reference) {
return true;
}
if is_documentation_like_start_entry_title(title)
&& !reference_points_to_executable_reference(reference)
{
return true;
}
false
}
#[cfg(target_os = "windows")]
fn discover_windows_search_items(
roots: &[PathBuf],
excluded_roots: &[PathBuf],
show_files: bool,
show_folders: bool,
max_items_total: usize,
max_items_per_root: usize,
) -> Result<Vec<SearchItem>, ProviderError> {
use std::collections::HashSet;
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let roots_joined = join_windows_paths_for_powershell(roots);
if roots_joined.is_empty() {
return Ok(Vec::new());
}
let exclusion_policy = DiscoveryExclusionPolicy::new(excluded_roots);
let effective_excluded_roots = effective_excluded_roots_for_powershell(excluded_roots);
let excluded_joined = join_windows_paths_for_powershell(&effective_excluded_roots);
let script = r#"
$ErrorActionPreference = 'Stop'
$separator = [char]0x1f
$roots = @()
$excludes = @()
if ($env:NEX_WS_ROOTS) { $roots = $env:NEX_WS_ROOTS -split $separator }
elseif ($env:SWIFTFIND_WS_ROOTS) { $roots = $env:SWIFTFIND_WS_ROOTS -split $separator }
if ($env:NEX_WS_EXCLUDES) { $excludes = $env:NEX_WS_EXCLUDES -split $separator }
elseif ($env:SWIFTFIND_WS_EXCLUDES) { $excludes = $env:SWIFTFIND_WS_EXCLUDES -split $separator }
$conn = New-Object -ComObject ADODB.Connection
$conn.Open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows'")
$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($root in $roots) {
if ([string]::IsNullOrWhiteSpace($root)) { continue }
$scope = $root.Trim().Replace('\', '/')
if (-not $scope.EndsWith('/')) { $scope += '/' }
$scope = $scope.Replace("'", "''")
$query = "SELECT System.ItemPathDisplay, System.ItemName, System.FileAttributes FROM SYSTEMINDEX WHERE scope='file:$scope'"
$recordset = $conn.Execute($query)
while (-not $recordset.EOF) {
$path = [string]$recordset.Fields.Item("System.ItemPathDisplay").Value
$name = [string]$recordset.Fields.Item("System.ItemName").Value
$attrsValue = $recordset.Fields.Item("System.FileAttributes").Value
$attrs = 0
if ($null -ne $attrsValue -and "$attrsValue" -ne "") { $attrs = [int64]$attrsValue }
if (-not [string]::IsNullOrWhiteSpace($path)) {
$skip = $false
foreach ($exclude in $excludes) {
if ([string]::IsNullOrWhiteSpace($exclude)) { continue }
if ($path.StartsWith($exclude, [System.StringComparison]::OrdinalIgnoreCase)) {
$skip = $true
break
}
}
if (-not $skip -and $seen.Add($path)) {
if ([string]::IsNullOrWhiteSpace($name)) { $name = [System.IO.Path]::GetFileName($path) }
if ([string]::IsNullOrWhiteSpace($name)) { $name = $path }
$kind = if (($attrs -band 16) -ne 0) { "folder" } else { "file" }
"{0}`t{1}`t{2}" -f $kind, $name, $path
}
}
$recordset.MoveNext()
}
$recordset.Close()
}
$conn.Close()
"#;
let mut command = Command::new("powershell.exe");
command
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-WindowStyle",
"Hidden",
"-Command",
script,
])
.env("NEX_WS_ROOTS", &roots_joined)
.env("SWIFTFIND_WS_ROOTS", roots_joined)
.env("NEX_WS_EXCLUDES", &excluded_joined)
.env("SWIFTFIND_WS_EXCLUDES", excluded_joined)
.creation_flags(CREATE_NO_WINDOW);
let output = command.output().map_err(|error| {
ProviderError::new(format!(
"Windows Search provider invocation failed: {error}"
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err(ProviderError::new(format!(
"Windows Search provider failed (status={}): {}",
output.status,
if stderr.is_empty() {
"no stderr"
} else {
stderr.as_str()
}
)));
}
let mut seen_ids = HashSet::new();
let normalized_roots = roots
.iter()
.map(|root| normalize_root_for_stamp(root))
.collect::<Vec<_>>();
let mut root_counts = vec![0_usize; normalized_roots.len()];
let total_budget = max_items_total.max(1);
let per_root_budget = max_items_per_root.max(1).min(total_budget);
let mut skipped_due_cap = 0_usize;
let mut skipped_due_exclusion = 0_usize;
let mut items = Vec::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let mut parts = line.splitn(3, '\t');
let Some(kind_raw) = parts.next() else {
continue;
};
let Some(title_raw) = parts.next() else {
continue;
};
let Some(path_raw) = parts.next() else {
continue;
};
let kind = kind_raw.trim().to_ascii_lowercase();
if kind != "file" && kind != "folder" {
continue;
}
if kind == "file" && !show_files {
continue;
}
if kind == "folder" && !show_folders {
continue;
}
let path = path_raw.trim();
if path.is_empty() {
continue;
}
let normalized_path = normalize_id_path(path);
let root_index = normalized_roots.iter().position(|root| {
normalized_path == *root
|| (normalized_path.starts_with(root)
&& normalized_path[root.len()..].starts_with('\\'))
});
let Some(root_index) = root_index else {
continue;
};
if exclusion_policy.should_exclude_path_under_root(Path::new(path), &roots[root_index]) {
skipped_due_exclusion = skipped_due_exclusion.saturating_add(1);
continue;
}
if items.len() >= total_budget || root_counts[root_index] >= per_root_budget {
skipped_due_cap = skipped_due_cap.saturating_add(1);
continue;
}
let title = title_raw.trim();
let display_title = if title.is_empty() { path } else { title };
let id = format!("{kind}:{normalized_path}");
if seen_ids.insert(id.clone()) {
items.push(SearchItem::new(&id, &kind, display_title, path));
root_counts[root_index] += 1;
}
}
if skipped_due_cap > 0 {
crate::logging::info(&format!(
"[nex] discovery_cap provider=windows_search skipped_due_cap={} total_cap={} per_root_cap={}",
skipped_due_cap, total_budget, per_root_budget
));
}
if skipped_due_exclusion > 0 {
crate::logging::info(&format!(
"[nex] discovery_exclusion provider=windows_search skipped={} policy_schema={}",
skipped_due_exclusion, FILESYSTEM_DISCOVERY_SCHEMA_VERSION
));
}
Ok(items)
}
#[cfg(target_os = "windows")]
fn effective_excluded_roots_for_powershell(user_excluded_roots: &[PathBuf]) -> Vec<PathBuf> {
let mut roots = builtin_exclusion_roots();
roots.extend(user_excluded_roots.iter().cloned());
roots.sort();
roots.dedup();
roots
}
#[cfg(target_os = "windows")]
fn join_windows_paths_for_powershell(paths: &[PathBuf]) -> String {
let mut out = Vec::new();
for path in paths {
let mut normalized = path.to_string_lossy().replace('/', "\\");
while normalized.ends_with('\\') && normalized.len() > 3 {
normalized.pop();
}
let trimmed = normalized.trim();
if !trimmed.is_empty() {
out.push(trimmed.to_string());
}
}
out.join("\u{1f}")
}
#[cfg(target_os = "windows")]
fn to_wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
#[cfg(target_os = "windows")]
fn pwstr_to_string_and_free(ptr: windows_sys::core::PWSTR) -> String {
use windows_sys::Win32::System::Com::CoTaskMemFree;
if ptr.is_null() {
return String::new();
}
let mut len = 0usize;
unsafe {
while *ptr.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(ptr, len);
let out = String::from_utf16_lossy(slice);
CoTaskMemFree(ptr as _);
out
}
}
#[cfg(target_os = "windows")]
fn shortcut_resolves_to_web_target(raw: &str) -> bool {
let lowered = raw.trim().trim_matches('"').to_ascii_lowercase();
if lowered.is_empty() {
return false;
}
lowered.starts_with("http://")
|| lowered.starts_with("https://")
|| lowered.starts_with("microsoft-edge:")
|| lowered.starts_with("msedge:")
|| lowered.starts_with("www.")
|| lowered.contains("://")
}
#[cfg(target_os = "windows")]
fn has_non_app_document_extension(value: &str) -> bool {
let normalized = normalize_shortcut_target_path(value).to_ascii_lowercase();
if normalized.is_empty() {
return false;
}
[
".url", ".pdf", ".htm", ".html", ".xhtml", ".mht", ".mhtml", ".chm", ".txt", ".md", ".rtf",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".csv", ".xml", ".json", ".yaml",
".yml", ".ini", ".log", ".php",
]
.iter()
.any(|ext| normalized.ends_with(ext))
}
#[cfg(target_os = "windows")]
fn reference_points_to_executable_reference(reference: &str) -> bool {
let normalized = normalize_shortcut_target_path(reference).to_ascii_lowercase();
if normalized.is_empty() {
return false;
}
if normalized.starts_with("shell:") || normalized.starts_with("ms-") {
return true;
}
[
".exe",
".com",
".bat",
".cmd",
".msc",
".ps1",
".vbs",
".appref-ms",
]
.iter()
.any(|ext| normalized.ends_with(ext))
}
#[cfg(target_os = "windows")]
fn normalize_shortcut_target_path(raw: &str) -> String {
raw.trim()
.trim_matches('"')
.trim_start_matches('@')
.trim()
.to_string()
}
#[cfg(target_os = "windows")]
fn looks_like_filesystem_path(path: &str) -> bool {
if path.starts_with('/') || path.starts_with('\\') {
return true;
}
let bytes = path.as_bytes();
bytes.len() >= 3 && bytes[1] == b':' && (bytes[2] == b'\\' || bytes[2] == b'/')
}
#[cfg(target_os = "windows")]
fn is_documentation_like_start_entry_title(title: &str) -> bool {
let lower = title.trim().to_ascii_lowercase();
if lower.is_empty() {
return false;
}
let has_docs = lower.contains("documentation") || lower.contains(" docs");
let has_sample = lower.contains("sample");
let has_tools_for = lower.contains("tools for");
let has_app_word = lower.contains(" app") || lower.contains("apps");
let has_platform = lower.contains("uwp")
|| lower.contains("desktop")
|| lower.contains("winui")
|| lower.contains("windows sdk");
(has_docs && has_app_word)
|| (has_sample && (has_app_word || has_platform))
|| (has_tools_for && has_app_word && has_platform)
}
#[cfg(target_os = "windows")]
fn is_excluded_windows_kits_shortcut_reference(value: &str) -> bool {
let lower = value.trim().replace('/', "\\").to_ascii_lowercase();
if lower.is_empty() {
return false;
}
if !lower.contains("\\windows kits\\10\\shortcuts\\") {
return false;
}
if !lower.ends_with(".url") {
return false;
}
lower.contains("sample")
|| lower.contains("documentation")
|| lower.contains("toolsdocumentation")
}