MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Generic;5using System.Globalization;6using System.IO;7using System.Management.Automation.Runspaces;8using System.Text;9 10using Dbg = System.Management.Automation.Diagnostics;11 12namespace System.Management.Automation.Internal13{14 internal static class ModuleUtils15 {16 // These are documented members FILE_ATTRIBUTE, they just have not yet been17 // added to System.IO.FileAttributes yet.18 private const int FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x400000;19 20 private const int FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x40000;21 22 // Default option for local file system enumeration:23 // - Ignore files/directories when access is denied;24 // - Search top directory only.25 private static readonly System.IO.EnumerationOptions s_defaultEnumerationOptions =26 new System.IO.EnumerationOptions() { AttributesToSkip = FileAttributesToSkip };27 28 private static readonly FileAttributes FileAttributesToSkip;29 30 // Default option for UNC path enumeration. Same as above plus a large buffer size.31 // For network shares, a large buffer may result in better performance as more results can be batched over the wire.32 // The buffer size 16K is recommended in the comment of the 'BufferSize' property:33 // "A "large" buffer, for example, would be 16K. Typical is 4K."34 private static readonly System.IO.EnumerationOptions s_uncPathEnumerationOptions =35 new System.IO.EnumerationOptions() { AttributesToSkip = FileAttributesToSkip, BufferSize = 16384 };36 37 private static readonly string EnCulturePath = Path.DirectorySeparatorChar + "en";38 private static readonly string EnUsCulturePath = Path.DirectorySeparatorChar + "en-us";39 40 static ModuleUtils()41 {42 FileAttributesToSkip = FileAttributes.Hidden43 // Skip OneDrive files/directories that are not fully on disk.44 | FileAttributes.Offline45 | (FileAttributes)FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS46 | (FileAttributes)FILE_ATTRIBUTE_RECALL_ON_OPEN;47 }48 49 /// <summary>50 /// Check if a directory is likely a localized resources folder.51 /// </summary>52 /// <param name="dir">Directory to check if it is a possible resource folder.</param>53 /// <returns>True if the directory name matches a culture.</returns>54 internal static bool IsPossibleResourceDirectory(string dir)55 {56 // Assume locale directories do not contain modules.57 if (dir.EndsWith(EnCulturePath, StringComparison.OrdinalIgnoreCase) ||58 dir.EndsWith(EnUsCulturePath, StringComparison.OrdinalIgnoreCase))59 {60 return true;61 }62 63 dir = Path.GetFileName(dir);64 65 // Use some simple pattern matching to avoid the call into GetCultureInfo when we know it will fail (and throw).66 if ((dir.Length == 2 && char.IsLetter(dir[0]) && char.IsLetter(dir[1]))67 ||68 (dir.Length == 5 && char.IsLetter(dir[0]) && char.IsLetter(dir[1]) && (dir[2] == '-') && char.IsLetter(dir[3]) && char.IsLetter(dir[4])))69 {70 try71 {72 // This might not throw on invalid culture still73 // 4096 is considered the unknown locale - so assume that could be a module74 var cultureInfo = new CultureInfo(dir);75 return cultureInfo.LCID != 4096;76 }77 catch { }78 }79 80 return false;81 }82 83 /// <summary>84 /// Get all module files by searching the given directory recursively.85 /// All sub-directories that could be a module folder will be searched.86 /// </summary>87 internal static IEnumerable<string> GetAllAvailableModuleFiles(string topDirectoryToCheck)88 {89 if (!Directory.Exists(topDirectoryToCheck)) { yield break; }90 91 var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions;92 Queue<string> directoriesToCheck = new Queue<string>();93 directoriesToCheck.Enqueue(topDirectoryToCheck);94 95 bool firstSubDirs = true;96 while (directoriesToCheck.Count > 0)97 {98 string directoryToCheck = directoriesToCheck.Dequeue();99 try100 {101 foreach (string toAdd in Directory.EnumerateDirectories(directoryToCheck, "*", options))102 {103 if (firstSubDirs || !IsPossibleResourceDirectory(toAdd))104 {105 directoriesToCheck.Enqueue(toAdd);106 }107 }108 }109 catch (IOException) { }110 catch (UnauthorizedAccessException) { }111 112 firstSubDirs = false;113 foreach (string moduleFile in Directory.EnumerateFiles(directoryToCheck, "*", options))114 {115 foreach (string ext in ModuleIntrinsics.PSModuleExtensions)116 {117 if (moduleFile.EndsWith(ext, StringComparison.OrdinalIgnoreCase))118 {119 yield return moduleFile;120 break; // one file can have only one extension121 }122 }123 }124 }125 }126 127 /// <summary>128 /// Check if the CompatiblePSEditions field of a given module129 /// declares compatibility with the running PowerShell edition.130 /// </summary>131 /// <param name="moduleManifestPath">The path to the module manifest being checked.</param>132 /// <param name="compatiblePSEditions">The value of the CompatiblePSEditions field of the module manifest.</param>133 /// <returns>True if the module is compatible with the running PowerShell edition, false otherwise.</returns>134 internal static bool IsPSEditionCompatible(135 string moduleManifestPath,136 IEnumerable<string> compatiblePSEditions)137 {138#if UNIX139 return true;140#else141 if (!IsOnSystem32ModulePath(moduleManifestPath))142 {143 return true;144 }145 146 return Utils.IsPSEditionSupported(compatiblePSEditions);147#endif148 }149 150 internal static IEnumerable<string> GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context)151 {152 HashSet<string> uniqueModuleFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);153 154 foreach (string directory in ModuleIntrinsics.GetModulePath(isForAutoDiscovery, context))155 {156 var needWriteProgressCompleted = false;157 ProgressRecord analysisProgress = null;158 159 // Write a progress message for UNC paths, so that users know what is happening160 try161 {162 if ((context.CurrentCommandProcessor != null) && Utils.PathIsUnc(directory))163 {164 analysisProgress = new ProgressRecord(0,165 Modules.DeterminingAvailableModules,166 string.Format(CultureInfo.InvariantCulture, Modules.SearchingUncShare, directory))167 {168 RecordType = ProgressRecordType.Processing169 };170 171 context.CurrentCommandProcessor.CommandRuntime.WriteProgress(analysisProgress);172 needWriteProgressCompleted = true;173 }174 }175 catch (InvalidOperationException)176 {177 // This may be called when we are not allowed to write progress,178 // So eat the invalid operation179 }180 181 try182 {183 foreach (string moduleFile in ModuleUtils.GetDefaultAvailableModuleFiles(directory))184 {185 if (uniqueModuleFiles.Add(moduleFile))186 {187 yield return moduleFile;188 }189 }190 }191 finally192 {193 if (needWriteProgressCompleted)194 {195 analysisProgress.RecordType = ProgressRecordType.Completed;196 context.CurrentCommandProcessor.CommandRuntime.WriteProgress(analysisProgress);197 }198 }199 }200 }201 202 /// <summary>203 /// Get a list of module files from the given directory without recursively searching all sub-directories.204 /// This method assumes the given directory is a module folder or a version sub-directory of a module folder.205 /// </summary>206 internal static List<string> GetModuleFilesFromAbsolutePath(string directory)207 {208 List<string> result = new List<string>();209 string fileName = Path.GetFileName(directory);210 211 // If the given directory doesn't exist or it's the root folder, then return an empty list.212 if (!Directory.Exists(directory) || string.IsNullOrEmpty(fileName)) { return result; }213 214 // If the user give the module path including version, the module name could be the parent folder name.215 if (Version.TryParse(fileName, out Version ver))216 {217 string parentDirPath = Path.GetDirectoryName(directory);218 string parentDirName = Path.GetFileName(parentDirPath);219 220 // If the parent directory is NOT a root folder, then it could be the module folder.221 if (!string.IsNullOrEmpty(parentDirName))222 {223 string manifestPath = Path.Combine(directory, parentDirName);224 manifestPath += StringLiterals.PowerShellDataFileExtension;225 if (File.Exists(manifestPath) && ver.Equals(ModuleIntrinsics.GetManifestModuleVersion(manifestPath)))226 {227 result.Add(manifestPath);228 return result;229 }230 }231 }232 233 // If we reach here, then use the given directory as the module folder.234 foreach (Version version in GetModuleVersionSubfolders(directory))235 {236 string manifestPath = Path.Combine(directory, version.ToString(), fileName);237 manifestPath += StringLiterals.PowerShellDataFileExtension;238 if (File.Exists(manifestPath) && version.Equals(ModuleIntrinsics.GetManifestModuleVersion(manifestPath)))239 {240 result.Add(manifestPath);241 }242 }243 244 foreach (string ext in ModuleIntrinsics.PSModuleExtensions)245 {246 string moduleFile = Path.Combine(directory, fileName) + ext;247 if (File.Exists(moduleFile))248 {249 result.Add(moduleFile);250 251 // when finding the default modules we stop when the first252 // match is hit - searching in order .psd1, .psm1, .dll,253 // if a file is found but is not readable then it is an error.254 break;255 }256 }257 258 return result;259 }260 261 /// <summary>262 /// Get a list of the available module files from the given directory.263 /// Search all module folders under the specified directory, but do not search sub-directories under a module folder.264 /// </summary>265 internal static IEnumerable<string> GetDefaultAvailableModuleFiles(string topDirectoryToCheck)266 {267 if (!Directory.Exists(topDirectoryToCheck)) { yield break; }268 269 var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions;270 List<Version> versionDirectories = new List<Version>();271 LinkedList<string> directoriesToCheck = new LinkedList<string>();272 directoriesToCheck.AddLast(topDirectoryToCheck);273 274 while (directoriesToCheck.Count > 0)275 {276 versionDirectories.Clear();277 string[] subdirectories;278 string directoryToCheck = directoriesToCheck.First.Value;279 directoriesToCheck.RemoveFirst();280 try281 {282 subdirectories = Directory.GetDirectories(directoryToCheck, "*", options);283 ProcessPossibleVersionSubdirectories(subdirectories, versionDirectories);284 }285 catch (IOException) { subdirectories = Array.Empty<string>(); }286 catch (UnauthorizedAccessException) { subdirectories = Array.Empty<string>(); }287 288 bool isModuleDirectory = false;289 string proposedModuleName = Path.GetFileName(directoryToCheck);290 foreach (Version version in versionDirectories)291 {292 string manifestPath = Path.Combine(directoryToCheck, version.ToString(), proposedModuleName);293 manifestPath += StringLiterals.PowerShellDataFileExtension;294 if (File.Exists(manifestPath))295 {296 if (HasSkippedFileAttribute(manifestPath))297 {298 continue;299 }300 301 isModuleDirectory = true;302 yield return manifestPath;303 }304 }305 306 if (!isModuleDirectory)307 {308 foreach (string ext in ModuleIntrinsics.PSModuleExtensions)309 {310 string moduleFile = Path.Combine(directoryToCheck, proposedModuleName) + ext;311 if (File.Exists(moduleFile))312 {313 if (HasSkippedFileAttribute(moduleFile))314 {315 continue;316 }317 318 isModuleDirectory = true;319 yield return moduleFile;320 321 // when finding the default modules we stop when the first322 // match is hit - searching in order .psd1, .psm1, .dll, .exe323 // if a file is found but is not readable then it is an324 // error325 break;326 }327 }328 }329 330 if (!isModuleDirectory)331 {332 foreach (var subdirectory in subdirectories)333 {334 if (subdirectory.EndsWith("Microsoft.PowerShell.Management", StringComparison.OrdinalIgnoreCase) ||335 subdirectory.EndsWith("Microsoft.PowerShell.Utility", StringComparison.OrdinalIgnoreCase))336 {337 directoriesToCheck.AddFirst(subdirectory);338 }339 else340 {341 directoriesToCheck.AddLast(subdirectory);342 }343 }344 }345 }346 }347 348 /// <summary>349 /// Gets the list of versions under the specified module base path in descending sorted order.350 /// </summary>351 /// <param name="moduleBase">Module base path.</param>352 /// <returns>Sorted list of versions.</returns>353 internal static List<Version> GetModuleVersionSubfolders(string moduleBase)354 {355 var versionFolders = new List<Version>();356 357 if (!string.IsNullOrWhiteSpace(moduleBase) && Directory.Exists(moduleBase))358 {359 var options = Utils.PathIsUnc(moduleBase) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions;360 IEnumerable<string> subdirectories = Directory.EnumerateDirectories(moduleBase, "*", options);361 ProcessPossibleVersionSubdirectories(subdirectories, versionFolders);362 }363 364 return versionFolders;365 }366 367 private static bool HasSkippedFileAttribute(string path)368 {369 try370 {371 FileAttributes attributes = File.GetAttributes(path);372 if ((attributes & FileAttributesToSkip) is not 0)373 {374 return true;375 }376 }377 catch378 {379 // Ignore failures so that we keep the current behavior of failing380 // later in the search.381 }382 383 return false;384 }385 386 private static void ProcessPossibleVersionSubdirectories(IEnumerable<string> subdirectories, List<Version> versionFolders)387 {388 foreach (string subdir in subdirectories)389 {390 string subdirName = Path.GetFileName(subdir);391 if (Version.TryParse(subdirName, out Version version))392 {393 versionFolders.Add(version);394 }395 }396 397 if (versionFolders.Count > 1)398 {399 versionFolders.Sort(static (x, y) => y.CompareTo(x));400 }401 }402 403 internal static bool IsModuleInVersionSubdirectory(string modulePath, out Version version)404 {405 version = null;406 string folderName = Path.GetDirectoryName(modulePath);407 if (folderName != null)408 {409 folderName = Path.GetFileName(folderName);410 return Version.TryParse(folderName, out version);411 }412 413 return false;414 }415 416 internal static bool IsOnSystem32ModulePath(string path)417 {418#if UNIX419 return false;420#else421 Dbg.Assert(!string.IsNullOrEmpty(path), $"Caller to verify that {nameof(path)} is not null or empty");422 423 string windowsPowerShellPSHomePath = ModuleIntrinsics.GetWindowsPowerShellPSHomeModulePath();424 return path.StartsWith(windowsPowerShellPSHomePath, StringComparison.OrdinalIgnoreCase);425#endif426 }427 428 /// <summary>429 /// Gets a list of fuzzy matching commands and their scores.430 /// </summary>431 /// <param name="pattern">Command pattern.</param>432 /// <param name="context">Execution context.</param>433 /// <param name="commandOrigin">Command origin.</param>434 /// <param name="fuzzyMatcher">Fuzzy matcher to use.</param>435 /// <param name="rediscoverImportedModules">If true, rediscovers imported modules.</param>436 /// <param name="moduleVersionRequired">Specific module version to be required.</param>437 /// <returns>IEnumerable tuple containing the CommandInfo and the match score.</returns>438 internal static IEnumerable<CommandScore> GetFuzzyMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, FuzzyMatcher fuzzyMatcher, bool rediscoverImportedModules = false, bool moduleVersionRequired = false)439 {440 foreach (CommandInfo command in GetMatchingCommands(pattern, context, commandOrigin, rediscoverImportedModules, moduleVersionRequired, fuzzyMatcher: fuzzyMatcher))441 {442 if (fuzzyMatcher.IsFuzzyMatch(command.Name, pattern, out int score))443 {444 yield return new CommandScore(command, score);445 }446 }447 }448 449 /// <summary>450 /// Gets a list of matching commands.451 /// </summary>452 /// <param name="pattern">Command pattern.</param>453 /// <param name="context">Execution context.</param>454 /// <param name="commandOrigin">Command origin.</param>455 /// <param name="rediscoverImportedModules">If true, rediscovers imported modules.</param>456 /// <param name="moduleVersionRequired">Specific module version to be required.</param>457 /// <param name="fuzzyMatcher">Fuzzy matcher for fuzzy searching.</param>458 /// <param name="useAbbreviationExpansion">Use abbreviation expansion for matching.</param>459 /// <returns>Returns matching CommandInfo IEnumerable.</returns>460 internal static IEnumerable<CommandInfo> GetMatchingCommands(string pattern, ExecutionContext context, CommandOrigin commandOrigin, bool rediscoverImportedModules = false, bool moduleVersionRequired = false, FuzzyMatcher fuzzyMatcher = null, bool useAbbreviationExpansion = false)461 {462 // Otherwise, if it had wildcards, just return the "AvailableCommand"463 // type of command info.464 WildcardPattern commandPattern = WildcardPattern.Get(pattern, WildcardOptions.IgnoreCase);465 466 CmdletInfo cmdletInfo = context.SessionState.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\\Get-Module");467 PSModuleAutoLoadingPreference moduleAutoLoadingPreference = CommandDiscovery.GetCommandDiscoveryPreference(context, SpecialVariables.PSModuleAutoLoadingPreferenceVarPath, "PSModuleAutoLoadingPreference");468 469 if ((moduleAutoLoadingPreference != PSModuleAutoLoadingPreference.None) &&470 ((commandOrigin == CommandOrigin.Internal) || ((cmdletInfo != null) && (cmdletInfo.Visibility == SessionStateEntryVisibility.Public))))471 {472 foreach (string modulePath in GetDefaultAvailableModuleFiles(isForAutoDiscovery: false, context))473 {474 // Skip modules that have already been loaded so that we don't expose private commands.475 string moduleName = Path.GetFileNameWithoutExtension(modulePath);476 List<PSModuleInfo> modules = context.Modules.GetExactMatchModules(moduleName, all: false, exactMatch: true);477 PSModuleInfo tempModuleInfo = null;478 479 if (modules.Count != 0)480 {481 // 1. We continue to the next module path if we don't want to re-discover those imported modules482 // 2. If we want to re-discover the imported modules, but one or more commands from the module were made private,483 // then we don't do re-discovery484 if (!rediscoverImportedModules || modules.Exists(static module => module.ModuleHasPrivateMembers))485 {486 continue;487 }488 489 if (modules.Count == 1)490 {491 PSModuleInfo psModule = modules[0];492 tempModuleInfo = new PSModuleInfo(psModule.Name, psModule.Path, context: null, sessionState: null);493 tempModuleInfo.SetModuleBase(psModule.ModuleBase);494 495 foreach (KeyValuePair<string, CommandInfo> entry in psModule.ExportedCommands)496 {497 if (commandPattern.IsMatch(entry.Value.Name) ||498 (fuzzyMatcher is not null && fuzzyMatcher.IsFuzzyMatch(entry.Value.Name, pattern)) ||499 (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(entry.Value.Name), StringComparison.OrdinalIgnoreCase)))500 {501 CommandInfo current = null;502 switch (entry.Value.CommandType)503 {504 case CommandTypes.Alias:505 current = new AliasInfo(entry.Value.Name, definition: null, context);506 break;507 case CommandTypes.Function:508 current = new FunctionInfo(entry.Value.Name, ScriptBlock.EmptyScriptBlock, context);509 break;510 case CommandTypes.Filter:511 current = new FilterInfo(entry.Value.Name, ScriptBlock.EmptyScriptBlock, context);512 break;513 case CommandTypes.Configuration:514 current = new ConfigurationInfo(entry.Value.Name, ScriptBlock.EmptyScriptBlock, context);515 break;516 case CommandTypes.Cmdlet:517 current = new CmdletInfo(entry.Value.Name, implementingType: null, helpFile: null, PSSnapin: null, context);518 break;519 default:520 Dbg.Assert(false, "cannot be hit");521 break;522 }523 524 current.Module = tempModuleInfo;525 yield return current;526 }527 }528 529 continue;530 }531 }532 533 string moduleShortName = Path.GetFileNameWithoutExtension(modulePath);534 535 IDictionary<string, CommandTypes> exportedCommands = AnalysisCache.GetExportedCommands(modulePath, testOnly: false, context);536 537 if (exportedCommands == null) { continue; }538 539 tempModuleInfo = new PSModuleInfo(moduleShortName, modulePath, sessionState: null, context: null);540 if (InitialSessionState.IsEngineModule(moduleShortName))541 {542 tempModuleInfo.SetModuleBase(Utils.DefaultPowerShellAppBase);543 }544 545 // moduleVersionRequired is bypassed by FullyQualifiedModule from calling method. This is the only place where guid will be involved.546 if (moduleVersionRequired && modulePath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))547 {548 tempModuleInfo.SetVersion(ModuleIntrinsics.GetManifestModuleVersion(modulePath));549 tempModuleInfo.SetGuid(ModuleIntrinsics.GetManifestGuid(modulePath));550 }551 552 foreach (KeyValuePair<string, CommandTypes> pair in exportedCommands)553 {554 string commandName = pair.Key;555 CommandTypes commandTypes = pair.Value;556 557 if (commandPattern.IsMatch(commandName) ||558 (fuzzyMatcher is not null && fuzzyMatcher.IsFuzzyMatch(commandName, pattern)) ||559 (useAbbreviationExpansion && string.Equals(pattern, AbbreviateName(commandName), StringComparison.OrdinalIgnoreCase)))560 {561 bool shouldExportCommand = true;562 563 // Verify that we don't already have it represented in the initial session state.564 if ((context.InitialSessionState != null) && (commandOrigin == CommandOrigin.Runspace))565 {566 foreach (SessionStateCommandEntry commandEntry in context.InitialSessionState.Commands[commandName])567 {568 string moduleCompareName = null;569 570 if (commandEntry.Module != null)571 {572 moduleCompareName = commandEntry.Module.Name;573 }574 else if (commandEntry.PSSnapIn != null)575 {576 moduleCompareName = commandEntry.PSSnapIn.Name;577 }578 579 if (string.Equals(moduleShortName, moduleCompareName, StringComparison.OrdinalIgnoreCase))580 {581 if (commandEntry.Visibility == SessionStateEntryVisibility.Private)582 {583 shouldExportCommand = false;584 }585 }586 }587 }588 589 if (shouldExportCommand)590 {591 if ((commandTypes & CommandTypes.Alias) == CommandTypes.Alias)592 {593 yield return new AliasInfo(commandName, null, context)594 {595 Module = tempModuleInfo596 };597 }598 599 if ((commandTypes & CommandTypes.Cmdlet) == CommandTypes.Cmdlet)600 {601 yield return new CmdletInfo(commandName, implementingType: null, helpFile: null, PSSnapin: null, context: context)602 {603 Module = tempModuleInfo604 };605 }606 607 if ((commandTypes & CommandTypes.Function) == CommandTypes.Function)608 {609 yield return new FunctionInfo(commandName, ScriptBlock.EmptyScriptBlock, context)610 {611 Module = tempModuleInfo612 };613 }614 615 if ((commandTypes & CommandTypes.Configuration) == CommandTypes.Configuration)616 {617 yield return new ConfigurationInfo(commandName, ScriptBlock.EmptyScriptBlock, context)618 {619 Module = tempModuleInfo620 };621 }622 }623 }624 }625 }626 }627 }628 629 /// <summary>630 /// Returns abbreviated version of a command name.631 /// </summary>632 /// <param name="commandName">Name of the command to transform.</param>633 /// <returns>Abbreviated version of the command name.</returns>634 internal static string AbbreviateName(string commandName)635 {636 // Use default size of 6 which represents expected average abbreviation length637 StringBuilder abbreviation = new StringBuilder(6);638 foreach (char c in commandName)639 {640 if (char.IsUpper(c) || c == '-')641 {642 abbreviation.Append(c);643 }644 }645 646 return abbreviation.ToString();647 }648 }649 650 internal struct CommandScore651 {652 public CommandScore(CommandInfo command, int score)653 {654 Command = command;655 Score = score;656 }657 658 public CommandInfo Command;659 public int Score;660 }661}662 