using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Search.Interop; namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex { public class QueryConstructor { private static readonly Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled); private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled); private Settings Settings { get; } private const string SystemIndex = "SystemIndex"; public QueryConstructor(Settings settings) { Settings = settings; } public CSearchQueryHelper CreateBaseQuery() { var baseQuery = CreateQueryHelper(); // Set the number of results we want. Don't set this property if all results are needed. baseQuery.QueryMaxResults = Settings.MaxResult; // Set list of columns we want to display, getting the path presently baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType"; // Filter based on file name baseQuery.QueryContentProperties = "System.FileName"; // Set sorting order baseQuery.QuerySorting = OrderIdentifier; return baseQuery; } internal static CSearchQueryHelper CreateQueryHelper() { // This uses the Microsoft.Search.Interop assembly // Throws COMException if Windows Search service is not running/disabled, this needs to be caught var manager = new CSearchManager(); // SystemIndex catalog is the default catalog in Windows var catalogManager = manager.GetCatalog(SystemIndex); // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer var queryHelper = catalogManager.GetQueryHelper(); return queryHelper; } public static string TopLevelDirectoryConstraint(ReadOnlySpan path) => $"directory='file:{path}'"; public static string RecursiveDirectoryConstraint(ReadOnlySpan path) => $"scope='file:{path}'"; /// /// Search will be performed on all folders and files on the first level of a specified directory. /// public string Directory(ReadOnlySpan path, ReadOnlySpan searchString = default, bool recursive = false) { var queryConstraint = searchString.IsWhiteSpace() ? "" : $" AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))"; var scopeConstraint = recursive ? RecursiveDirectoryConstraint(path) : TopLevelDirectoryConstraint(path); var baseQueryHelper = CreateBaseQuery(); baseQueryHelper.QueryWhereRestrictions = $"AND {scopeConstraint}{queryConstraint}"; return baseQueryHelper.GenerateSQLFromUserQuery("*"); } /// /// Search will be performed on all folders and files based on user's search keywords. /// public string FilesAndFolders(ReadOnlySpan userSearchString, IEnumerable allowedResultTypes = null) { if (userSearchString.IsWhiteSpace()) userSearchString = "*"; // Remove any special characters that might cause issues with the query var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString); var constraints = new List { RestrictionsForAllFilesAndFoldersSearch }; var typeConstraint = BuildTypeFilterConstraint(allowedResultTypes); if (!string.IsNullOrEmpty(typeConstraint)) constraints.Add(typeConstraint); var extensionConstraint = BuildExtensionExclusionConstraint(); if (!string.IsNullOrEmpty(extensionConstraint)) constraints.Add(extensionConstraint); var queryHelper = CreateBaseQuery(); queryHelper.QueryWhereRestrictions = $"AND {string.Join(" AND ", constraints)}"; return queryHelper.GenerateSQLFromUserQuery(replacedSearchString); } /// /// Build WHERE clause constraint to filter by result types (File, Folder, Volume). /// /// /// System.ItemType values: /// - ".directory" for folders /// - Specific file extensions (e.g., ".txt", ".pdf") for files /// - Drive volumes are identified by their path structure /// private static string BuildTypeFilterConstraint(IEnumerable allowedResultTypes) { if (allowedResultTypes == null) return null; var typesList = allowedResultTypes as IList ?? allowedResultTypes.ToList(); var hasFile = typesList.Contains(ResultType.File); var hasFolder = typesList.Contains(ResultType.Folder) || typesList.Contains(ResultType.Volume); // Folder and volume are merged in Folder action keyword so treat them as same // No filtering needed if empty or all types are allowed if (hasFile && hasFolder || !hasFile && !hasFolder) return null; if (hasFolder) return "System.ItemType = '.directory'"; if (hasFile) return "System.ItemType <> '.directory'"; return null; } /// /// Build WHERE clause constraint to exclude specific file extensions. /// /// Comma or semicolon separated file extensions without dots (e.g., "queryHelper,log,bak") private string BuildExtensionExclusionConstraint() { var extensions = Settings.ExcludedFileTypeList .Select(ext => $"System.FileExtension NOT LIKE '.{ext}'") .ToArray(); if (extensions.Length == 0) return ""; return string.Join(" AND ", extensions); } /// /// If one special character have white space on one side, replace it with one white space. /// So command will not have "[special character]+*" which will cause OLEDB exception. /// private static string ReplaceSpecialCharacterWithTwoSideWhiteSpace(ReadOnlySpan input) { const string whiteSpace = " "; var inputString = input.ToString(); // Use regex to match special characters with whitespace on one side // and replace them with a single space var result = _specialCharacterMatcher.Replace(inputString, match => { // Check if the match has whitespace on one side bool hasLeadingWhitespace = match.Index > 0 && char.IsWhiteSpace(inputString[match.Index - 1]); bool hasTrailingWhitespace = match.Index + match.Length < inputString.Length && char.IsWhiteSpace(inputString[match.Index + match.Length]); if (hasLeadingWhitespace || hasTrailingWhitespace) { return whiteSpace; } return match.Value; }); // Remove any extra spaces that might have been introduced return _multiWhiteSpacesMatcher.Replace(result, whiteSpace).Trim(); } /// /// Set the required WHERE clause restriction to search for all files and folders. /// public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'"; /// /// Order identifier: System.Search.Rank DESC /// /// /// /// public const string OrderIdentifier = "System.Search.Rank DESC"; /// /// Search will be performed on all indexed file contents for the specified search keywords. /// public string FileContent(ReadOnlySpan userSearchString) { var constraints = new List { RestrictionsForFileContentSearch(userSearchString), RestrictionsForAllFilesAndFoldersSearch }; var extensionConstraint = BuildExtensionExclusionConstraint(); if (!string.IsNullOrEmpty(extensionConstraint)) constraints.Add(extensionConstraint); var queryHelper = CreateBaseQuery(); queryHelper.QueryWhereRestrictions = $"AND {string.Join(" AND ", constraints)}"; return queryHelper.GenerateSQLFromUserQuery("*"); } /// /// Set the required WHERE clause restriction to search within file content. /// public static string RestrictionsForFileContentSearch(ReadOnlySpan searchQuery) => $"FREETEXT('{searchQuery}')"; } }