Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes308downloads
NativeCommandParameterBinder.cs521 linesDownload Raw Back to engine
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