MegaBites-AI/Windows-powershell
0308
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.Generic;6using System.Collections.ObjectModel;7using System.IO;8using System.Linq;9using System.Management.Automation.Internal;10using System.Text;11 12using Microsoft.PowerShell.Commands;13 14namespace System.Management.Automation15{16 using Language;17 18 /// <summary>19 /// The parameter binder for native commands.20 /// </summary>21 internal class NativeCommandParameterBinder : ParameterBinderBase22 {23 #region ctor24 25 /// <summary>26 /// Constructs a NativeCommandParameterBinder.27 /// </summary>28 /// <param name="command">29 /// The NativeCommand to bind to.30 /// </param>31 /// <exception cref="ArgumentNullException">32 /// <paramref name="command"/>.Context is null33 /// </exception>34 internal NativeCommandParameterBinder(35 NativeCommand command) : base(command.MyInvocation, command.Context, command)36 {37 _nativeCommand = command;38 }39 #endregion ctor40 41 #region internal members42 43 #region Parameter binding44 45 /// <summary>46 /// Binds a parameter for a native command (application).47 /// </summary>48 /// <param name="name">49 /// The name of the parameter to bind the value to. For applications50 /// this just becomes another parameter...51 /// </param>52 /// <param name="value">53 /// The value to bind to the parameter. It should be assumed by54 /// derived classes that the proper type coercion has already taken55 /// place and that any prerequisite metadata has been satisfied.56 /// </param>57 /// <param name="parameterMetadata"></param>58 internal override void BindParameter(string name, object value, CompiledCommandParameter parameterMetadata)59 {60 Diagnostics.Assert(false, "Unreachable code");61 62 throw new NotSupportedException();63 }64 65 internal override object GetDefaultParameterValue(string name)66 {67 return null;68 }69 70 internal void BindParameters(Collection<CommandParameterInternal> parameters)71 {72 bool sawVerbatimArgumentMarker = false;73 bool first = true;74 foreach (CommandParameterInternal parameter in parameters)75 {76 if (!first)77 {78 _arguments.Append(' ');79 }80 81 first = false;82 83 if (parameter.ParameterNameSpecified)84 {85 Diagnostics.Assert(!parameter.ParameterText.Contains(' '), "Parameters cannot have whitespace");86 PossiblyGlobArg(parameter.ParameterText, parameter, usedQuotes: false);87 88 if (parameter.SpaceAfterParameter)89 {90 _arguments.Append(' ');91 }92 }93 94 if (parameter.ArgumentSpecified)95 {96 // If this is the verbatim argument marker, we don't pass it on to the native command.97 // We do need to remember it though - we'll expand environment variables in subsequent args.98 object argValue = parameter.ArgumentValue;99 if (string.Equals("--%", argValue as string, StringComparison.OrdinalIgnoreCase))100 {101 sawVerbatimArgumentMarker = true;102 continue;103 }104 105 if (argValue != AutomationNull.Value && argValue != UnboundParameter.Value)106 {107 // ArrayLiteralAst is used to reconstruct the correct argument, e.g.108 // windbg -k com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect109 // The parser produced an array of strings but marked the parameter so we110 // can properly reconstruct the correct command line.111 bool usedQuotes = false;112 ArrayLiteralAst arrayLiteralAst = null;113 switch (parameter?.ArgumentAst)114 {115 case StringConstantExpressionAst sce:116 usedQuotes = sce.StringConstantType != StringConstantType.BareWord;117 break;118 case ExpandableStringExpressionAst ese:119 usedQuotes = ese.StringConstantType != StringConstantType.BareWord;120 break;121 case ArrayLiteralAst ala:122 arrayLiteralAst = ala;123 break;124 }125 126 AppendOneNativeArgument(Context, parameter, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, usedQuotes);127 }128 }129 }130 }131 132 #endregion Parameter binding133 134 /// <summary>135 /// Gets the command arguments in string form.136 /// </summary>137 internal string Arguments138 {139 get140 {141 return _arguments.ToString();142 }143 }144 145 private readonly StringBuilder _arguments = new StringBuilder();146 147 internal string[] ArgumentList148 {149 get150 {151 return _argumentList.ToArray();152 }153 }154 155 /// <summary>156 /// Add an argument to the ArgumentList.157 /// We may need to construct the argument out of the parameter text and the argument158 /// in the case that we have a parameter that appears as "-switch:value".159 /// </summary>160 /// <param name="parameter">The parameter associated with the operation.</param>161 /// <param name="argument">The value used with parameter.</param>162 internal void AddToArgumentList(CommandParameterInternal parameter, string argument)163 {164 if (parameter.ParameterNameSpecified && parameter.ParameterText.EndsWith(':'))165 {166 if (argument != parameter.ParameterText)167 {168 // Only combine the text and argument if there was no space after the parameter,169 // otherwise, add the parameter and arguments as separate elements.170 if (parameter.SpaceAfterParameter)171 {172 _argumentList.Add(parameter.ParameterText);173 _argumentList.Add(argument);174 }175 else176 {177 _argumentList.Add(parameter.ParameterText + argument);178 }179 }180 }181 else182 {183 _argumentList.Add(argument);184 }185 }186 187 private readonly List<string> _argumentList = new List<string>();188 189 /// <summary>190 /// Gets a value indicating whether to use an ArgumentList or string for arguments when invoking a native executable.191 /// </summary>192 internal NativeArgumentPassingStyle ArgumentPassingStyle193 {194 get195 {196 try197 {198 var preference = LanguagePrimitives.ConvertTo<NativeArgumentPassingStyle>(199 Context.GetVariableValue(SpecialVariables.NativeArgumentPassingVarPath, NativeArgumentPassingStyle.Standard));200 return preference;201 }202 catch203 {204 // The value is not convertible send back Legacy205 return NativeArgumentPassingStyle.Legacy;206 }207 }208 }209 210 #endregion internal members211 212 #region private members213 214 /// <summary>215 /// Stringize a non-IEnum argument to a native command, adding quotes216 /// and trailing spaces as appropriate. An array gets added as multiple arguments217 /// each of which will be stringized.218 /// </summary>219 /// <param name="context">Execution context instance.</param>220 /// <param name="parameter">The parameter associated with the operation.</param>221 /// <param name="obj">The object to append.</param>222 /// <param name="argArrayAst">If the argument was an array literal, the Ast, otherwise null.</param>223 /// <param name="sawVerbatimArgumentMarker">True if the argument occurs after --%.</param>224 /// <param name="usedQuotes">True if the argument was a quoted string (single or double).</param>225 private void AppendOneNativeArgument(ExecutionContext context, CommandParameterInternal parameter, object obj, ArrayLiteralAst argArrayAst, bool sawVerbatimArgumentMarker, bool usedQuotes)226 {227 IEnumerator list = LanguagePrimitives.GetEnumerator(obj);228 229 Diagnostics.Assert((argArrayAst == null) || (obj is object[] && ((object[])obj).Length == argArrayAst.Elements.Count), "array argument and ArrayLiteralAst differ in number of elements");230 231 int currentElement = -1;232 string separator = string.Empty;233 do234 {235 string arg;236 object currentObj;237 if (list == null)238 {239 arg = PSObject.ToStringParser(context, obj);240 currentObj = obj;241 }242 else243 {244 if (!ParserOps.MoveNext(context, null, list))245 {246 break;247 }248 249 currentObj = ParserOps.Current(null, list);250 arg = PSObject.ToStringParser(context, currentObj);251 252 currentElement += 1;253 if (currentElement != 0)254 {255 separator = GetEnumerableArgSeparator(argArrayAst, currentElement);256 }257 }258 259 if (!string.IsNullOrEmpty(arg))260 {261 // Only add the separator to the argument string rather than adding a separator to the ArgumentList.262 _arguments.Append(separator);263 264 if (sawVerbatimArgumentMarker)265 {266 arg = Environment.ExpandEnvironmentVariables(arg);267 _arguments.Append(arg);268 269 // we need to split the argument on spaces270 _argumentList.AddRange(arg.Split(' ', StringSplitOptions.RemoveEmptyEntries));271 }272 else273 {274 // We need to add quotes if the argument has unquoted spaces. The275 // quotes could appear anywhere inside the string, not just at the start,276 // e.g.277 // $a = 'a"b c"d'278 // echoargs $a 'a"b c"d' a"b c"d279 //280 // The above should see 3 identical arguments in argv (the command line will281 // actually have quotes in different places, but the Win32 command line=>argv parser282 // erases those differences.283 //284 // We need to check quotes that the win32 argument parser checks which is currently285 // just the normal double quotes, no other special quotes. Also note that mismatched286 // quotes are supported287 if (NeedQuotes(arg))288 {289 _arguments.Append('"');290 AddToArgumentList(parameter, arg);291 292 // need to escape all trailing backslashes so the native command receives it correctly293 // according to http://www.daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESDOC294 _arguments.Append(arg);295 for (int i = arg.Length - 1; i >= 0 && arg[i] == '\\'; i--)296 {297 _arguments.Append('\\');298 }299 300 _arguments.Append('"');301 }302 else303 {304 if (argArrayAst != null && ArgumentPassingStyle != NativeArgumentPassingStyle.Legacy)305 {306 // We have a literal array, so take the extent, break it on spaces and add them to the argument list.307 foreach (string element in argArrayAst.Extent.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries))308 {309 PossiblyGlobArg(element, parameter, usedQuotes);310 }311 312 break;313 }314 else315 {316 PossiblyGlobArg(arg, parameter, usedQuotes);317 }318 }319 }320 }321 else if (ArgumentPassingStyle != NativeArgumentPassingStyle.Legacy && currentObj != null)322 {323 // add empty strings to arglist, but not nulls324 AddToArgumentList(parameter, arg);325 }326 }327 while (list != null);328 }329 330 /// <summary>331 /// On Windows, do tilde expansion, otherwise just append <paramref name="arg"/>.332 /// On Unix, do globbing as appropriate, otherwise just append <paramref name="arg"/>.333 /// </summary>334 /// <param name="arg">The argument that possibly needs expansion.</param>335 /// <param name="parameter">The parameter associated with the operation.</param>336 /// <param name="usedQuotes">True if the argument was a quoted string (single or double).</param>337 private void PossiblyGlobArg(string arg, CommandParameterInternal parameter, bool usedQuotes)338 {339 var argExpanded = false;340 341#if UNIX342 // On UNIX systems, we expand arguments containing wildcard expressions against343 // the file system just like bash, etc.344 if (!usedQuotes && WildcardPattern.ContainsWildcardCharacters(arg))345 {346 // See if the current working directory is a filesystem provider location347 // We won't do the expansion if it isn't since native commands can only access the file system.348 var cwdinfo = Context.EngineSessionState.CurrentLocation;349 350 // If it's a filesystem location then expand the wildcards351 if (cwdinfo.Provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase))352 {353 // On UNIX, paths starting with ~ or absolute paths are not normalized354 bool normalizePath = arg.Length == 0 || !(arg[0] == '~' || arg[0] == '/');355 356 // See if there are any matching paths otherwise just add the pattern as the argument357 Collection<PSObject> paths = null;358 try359 {360 paths = Context.EngineSessionState.InvokeProvider.ChildItem.Get(arg, false);361 }362 catch363 {364 // Fallthrough will append the pattern unchanged.365 }366 367 // Expand paths, but only from the file system.368 if (paths?.Count > 0 && paths.All(p => p.BaseObject is FileSystemInfo))369 {370 var sep = string.Empty;371 foreach (var path in paths)372 {373 _arguments.Append(sep);374 sep = " ";375 var expandedPath = (path.BaseObject as FileSystemInfo).FullName;376 if (normalizePath)377 {378 expandedPath =379 Context.SessionState.Path.NormalizeRelativePath(expandedPath, cwdinfo.ProviderPath);380 }381 // If the path contains spaces, then add quotes around it.382 if (NeedQuotes(expandedPath))383 {384 _arguments.Append('"');385 _arguments.Append(expandedPath);386 _arguments.Append('"');387 }388 else389 {390 _arguments.Append(expandedPath);391 }392 393 AddToArgumentList(parameter, expandedPath);394 argExpanded = true;395 }396 }397 }398 }399 else if (!usedQuotes)400 {401 // Even if there are no wildcards, we still need to possibly402 // expand ~ into the filesystem provider home directory path403 if (ExpandTilde(arg, parameter))404 {405 argExpanded = true;406 }407 }408#else409 if (!usedQuotes && ExpandTilde(arg, parameter))410 {411 argExpanded = true;412 }413#endif414 415 if (!argExpanded)416 {417 _arguments.Append(arg);418 AddToArgumentList(parameter, arg);419 }420 }421 422 /// <summary>423 /// Replace tilde for unquoted arguments in the form ~ and ~/. For windows, ~\ is also expanded.424 /// </summary>425 /// <param name="arg">The argument that possibly needs expansion.</param>426 /// <param name="parameter">The parameter associated with the operation.</param>427 /// <returns>True if tilde expansion occurred.</returns>428 private bool ExpandTilde(string arg, CommandParameterInternal parameter)429 {430 var fileSystemProvider = Context.EngineSessionState.GetSingleProvider(FileSystemProvider.ProviderName);431 var home = fileSystemProvider.Home;432 if (string.Equals(arg, "~"))433 {434 _arguments.Append(home);435 AddToArgumentList(parameter, home);436 return true;437 }438 else if (arg.StartsWith("~/") || (OperatingSystem.IsWindows() && arg.StartsWith(@"~\")))439 {440 var replacementString = string.Concat(home, arg.AsSpan(1));441 _arguments.Append(replacementString);442 AddToArgumentList(parameter, replacementString);443 return true;444 }445 446 return false;447 }448 449 /// <summary>450 /// Check to see if the string contains spaces and therefore must be quoted.451 /// </summary>452 /// <param name="stringToCheck">The string to check for spaces.</param>453 internal static bool NeedQuotes(string stringToCheck)454 {455 bool needQuotes = false, followingBackslash = false;456 int quoteCount = 0;457 for (int i = 0; i < stringToCheck.Length; i++)458 {459 if (stringToCheck[i] == '"' && !followingBackslash)460 {461 quoteCount += 1;462 }463 else if (char.IsWhiteSpace(stringToCheck[i]) && (quoteCount % 2 == 0))464 {465 needQuotes = true;466 }467 468 followingBackslash = stringToCheck[i] == '\\';469 }470 471 return needQuotes;472 }473 474 private static string GetEnumerableArgSeparator(ArrayLiteralAst arrayLiteralAst, int index)475 {476 if (arrayLiteralAst == null)477 {478 return " ";479 }480 481 // index points to the *next* element, so we're looking for space between482 // it and the previous element.483 484 var next = arrayLiteralAst.Elements[index];485 var prev = arrayLiteralAst.Elements[index - 1];486 487 var arrayExtent = arrayLiteralAst.Extent;488 var afterPrev = prev.Extent.EndOffset;489 var beforeNext = next.Extent.StartOffset - 1;490 491 if (afterPrev == beforeNext)492 {493 return ",";494 }495 496 var arrayText = arrayExtent.Text;497 afterPrev -= arrayExtent.StartOffset;498 beforeNext -= arrayExtent.StartOffset;499 500 if (arrayText[afterPrev] == ',')501 {502 return ", ";503 }504 505 if (arrayText[beforeNext] == ',')506 {507 return " ,";508 }509 510 return " , ";511 }512 513 /// <summary>514 /// The native command to bind to.515 /// </summary>516 private readonly NativeCommand _nativeCommand;517 518 #endregion private members519 }520}521 