MegaBites-AI/Windows-powershell
0308
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Generic;5using System.Collections.ObjectModel;6using System.Globalization;7using System.Linq;8using System.Management.Automation.Language;9using System.Management.Automation.Runspaces;10 11namespace System.Management.Automation12{13 // A visitor to walk an AST and validate that it can be converted to PowerShell.14 internal class ScriptBlockToPowerShellChecker : AstVisitor15 {16 private readonly HashSet<string> _validVariables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);17 18 internal ScriptBlockAst ScriptBeingConverted { get; set; }19 20 internal bool UsesParameter { get; private set; }21 22 internal bool HasUsingExpr { get; private set; }23 24 public override AstVisitAction VisitParameter(ParameterAst parameterAst)25 {26 if (parameterAst.Name.VariablePath.IsAnyLocal())27 {28 _validVariables.Add(parameterAst.Name.VariablePath.UnqualifiedPath);29 }30 31 return AstVisitAction.Continue;32 }33 34 public override AstVisitAction VisitPipeline(PipelineAst pipelineAst)35 {36 if (pipelineAst.PipelineElements[0] is CommandExpressionAst)37 {38 // If the first element is a CommandExpression, this pipeline should be the value39 // of a parameter. We want to avoid a scriptblock that contains only a pure expression.40 // The check "pipelineAst.Parent.Parent == ScriptBeingConverted" guarantees we throw41 // error on that kind of scriptblock.42 43 // Disallow pure expressions at the "top" level, but allow them otherwise.44 // We want to catch:45 // 1 | echo46 // But we don't want to error out on:47 // echo $(1)48 // See the comment in VisitCommand on why it's safe to check Parent.Parent, we49 // know that we have at least:50 // * a NamedBlockAst (the end block)51 // * a ScriptBlockAst (the ast we're comparing to)52 if (pipelineAst.GetPureExpression() == null || pipelineAst.Parent.Parent == ScriptBeingConverted)53 {54 ThrowError(55 new ScriptBlockToPowerShellNotSupportedException(56 "CantConvertPipelineStartsWithExpression", null,57 AutomationExceptions.CantConvertPipelineStartsWithExpression),58 pipelineAst);59 }60 }61 62 return AstVisitAction.Continue;63 }64 65 public override AstVisitAction VisitCommand(CommandAst commandAst)66 {67 if (commandAst.InvocationOperator == TokenKind.Dot)68 {69 ThrowError(70 new ScriptBlockToPowerShellNotSupportedException(71 "CantConvertWithDotSourcing", null, AutomationExceptions.CantConvertWithDotSourcing),72 commandAst);73 }74 75 // Up front checking ensures that we have a simple script block,76 // so we can safely assume that the parents are:77 // * a PipelineAst78 // * a NamedBlockAst (the end block)79 // * a ScriptBlockAst (the ast we're comparing to)80 // If that isn't the case, the conversion isn't allowed. It81 // is also safe to assume that we have at least 3 parents, a script block can't be simpler.82 if (commandAst.Parent.Parent.Parent != ScriptBeingConverted)83 {84 ThrowError(85 new ScriptBlockToPowerShellNotSupportedException(86 "CantConvertWithCommandInvocations", null, AutomationExceptions.CantConvertWithCommandInvocations),87 commandAst);88 }89 90 if (commandAst.CommandElements[0] is ScriptBlockExpressionAst)91 {92 ThrowError(93 new ScriptBlockToPowerShellNotSupportedException(94 "CantConvertWithScriptBlockInvocation", null,95 AutomationExceptions.CantConvertWithScriptBlockInvocation),96 commandAst);97 }98 99 return AstVisitAction.Continue;100 }101 102 public override AstVisitAction VisitMergingRedirection(MergingRedirectionAst redirectionAst)103 {104 if (redirectionAst.ToStream != RedirectionStream.Output)105 {106 ThrowError(107 new ScriptBlockToPowerShellNotSupportedException(108 "CanConvertOneOutputErrorRedir", null, AutomationExceptions.CanConvertOneOutputErrorRedir),109 redirectionAst);110 }111 112 return AstVisitAction.Continue;113 }114 115 public override AstVisitAction VisitFileRedirection(FileRedirectionAst redirectionAst)116 {117 ThrowError(118 new ScriptBlockToPowerShellNotSupportedException(119 "CanConvertOneOutputErrorRedir", null, AutomationExceptions.CanConvertOneOutputErrorRedir),120 redirectionAst);121 122 return AstVisitAction.Continue;123 }124 125 public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)126 {127 bool usesParameterReference = this.UsesParameter;128 bool ok = variableExpressionAst.IsSafeVariableReference(_validVariables, ref usesParameterReference);129 if (usesParameterReference != this.UsesParameter)130 {131 this.UsesParameter = usesParameterReference;132 }133 134 if (!ok)135 {136 ThrowError(new ScriptBlockToPowerShellNotSupportedException(137 "CantConvertWithUndeclaredVariables",138 null,139 AutomationExceptions.CantConvertWithUndeclaredVariables,140 variableExpressionAst.VariablePath),141 variableExpressionAst);142 }143 144 return AstVisitAction.Continue;145 }146 147 public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)148 {149 ThrowError(new ScriptBlockToPowerShellNotSupportedException(150 "CantConvertWithScriptBlocks",151 null,152 AutomationExceptions.CantConvertWithScriptBlocks),153 scriptBlockExpressionAst);154 155 return AstVisitAction.SkipChildren;156 }157 158 public override AstVisitAction VisitUsingExpression(UsingExpressionAst usingExpressionAst)159 {160 // A using expression is always allowed, it simply gets re-written to be a parameter161 HasUsingExpr = true;162 163 // Skip the children - the expression is evaluated before sending to the remote machine,164 // so it doesn't matter what we might find in the children.165 return AstVisitAction.SkipChildren;166 }167 168 internal static void ThrowError(ScriptBlockToPowerShellNotSupportedException ex, Ast ast)169 {170 InterpreterError.UpdateExceptionErrorRecordPosition(ex, ast.Extent);171 throw ex;172 }173 }174 175 internal sealed class UsingExpressionAstSearcher : AstSearcher176 {177 internal static IEnumerable<Ast> FindAllUsingExpressions(Ast ast)178 {179 Diagnostics.Assert(ast != null, "caller to verify arguments");180 181 var searcher = new UsingExpressionAstSearcher(182 callback: astParam => astParam is UsingExpressionAst,183 stopOnFirst: false,184 searchNestedScriptBlocks: true);185 ast.InternalVisit(searcher);186 return searcher.Results;187 }188 189 private UsingExpressionAstSearcher(Func<Ast, bool> callback, bool stopOnFirst, bool searchNestedScriptBlocks)190 : base(callback, stopOnFirst, searchNestedScriptBlocks)191 {192 }193 194 public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast)195 {196 // Skip the workflow. We are not interested in the UsingExpressions in a workflow197 if (ast.IsWorkflow)198 {199 return AstVisitAction.SkipChildren;200 }201 202 return CheckScriptBlock(ast);203 }204 }205 206 /// <summary>207 /// Converts a ScriptBlock to a PowerShell object by traversing the208 /// given Ast.209 /// </summary>210 internal sealed class ScriptBlockToPowerShellConverter211 {212 private readonly PowerShell _powershell;213 private ExecutionContext _context;214 private Dictionary<string, object> _usingValueMap;215 private bool? _createLocalScope;216 217 private ScriptBlockToPowerShellConverter()218 {219 _powershell = PowerShell.Create();220 }221 222 internal static PowerShell Convert(ScriptBlockAst body,223 ReadOnlyCollection<ParameterAst> functionParameters,224 bool isTrustedInput,225 ExecutionContext context,226 Dictionary<string, object> variables,227 bool filterNonUsingVariables,228 bool? createLocalScope,229 object[] args)230 {231 ExecutionContext.CheckStackDepth();232 233 args ??= Array.Empty<object>();234 235 // Perform validations on the ScriptBlock. GetSimplePipeline can allow for more than one236 // pipeline if the first parameter is true, but Invoke-Command doesn't yet support multiple237 // pipelines in a PowerShell (it just grabs the last command directly.) The rest of this238 // code properly supports multiple pipelines, so it should just work to change the false to true239 // if/when Invoke-Command can support multiple pipelines.240 string errorId;241 string errorMsg;242 body.GetSimplePipeline(true, out errorId, out errorMsg);243 if (errorId != null)244 {245 throw new ScriptBlockToPowerShellNotSupportedException(errorId, null, errorMsg);246 }247 248 var checker = new ScriptBlockToPowerShellChecker { ScriptBeingConverted = body };249 if (functionParameters != null)250 {251 foreach (var parameter in functionParameters)252 {253 parameter.InternalVisit(checker);254 }255 }256 257 body.InternalVisit(checker);258 259 // When the context is null (or they haven't supplied any variables), throw, but only if we really need the260 // context (basically, if we have some variable reference to resolve).261 if (context == null && (checker.HasUsingExpr || checker.UsesParameter) && (variables == null))262 {263 throw new PSInvalidOperationException(AutomationExceptions.CantConvertScriptBlockWithNoContext);264 }265 266 try267 {268 var converter = new ScriptBlockToPowerShellConverter { _context = context, _createLocalScope = createLocalScope };269 270 if (checker.HasUsingExpr)271 {272 converter._usingValueMap = GetUsingValues(body, isTrustedInput, context, variables, filterNonUsingVariables).Item1;273 }274 275 if (checker.UsesParameter)276 {277 // If any parameters are used, we create a new scope and bind the parameters.278 279 var newScope = context.EngineSessionState.NewScope(false);280 context.EngineSessionState.CurrentScope = newScope;281 context.EngineSessionState.CurrentScope.ScopeOrigin = CommandOrigin.Internal;282 283 var locals =284 MutableTuple.MakeTuple(Compiler.DottedLocalsTupleType, Compiler.DottedLocalsNameIndexMap);285 286 // Get the parameter metadata for the script block.287 // If 'functionParameters' is not null, then the ScriptBlockAst is actually the body of a FunctionDefinitionAst, and it doesn't have a ParamBlock.288 // If 'functionParameters' is null, then the ScriptBlockAst may have parameters defined in its ParamBlock.289 bool usesCmdletBinding = false;290 var parameters = functionParameters != null291 ? Compiler.GetParameterMetaData(functionParameters, true, ref usesCmdletBinding)292 : ((IParameterMetadataProvider)body).GetParameterMetadata(true, ref usesCmdletBinding);293 object[] remainingArgs = ScriptBlock.BindArgumentsForScriptblockInvoke(294 (RuntimeDefinedParameter[])parameters.Data, args, context, false, null, locals);295 locals.SetAutomaticVariable(AutomaticVariable.Args, remainingArgs, context);296 newScope.LocalsTuple = locals;297 }298 299 foreach (var pipeline in body.EndBlock.Statements.OfType<PipelineAst>())300 {301 converter._powershell.AddStatement();302 converter.ConvertPipeline(pipeline, isTrustedInput);303 }304 305 return converter._powershell;306 }307 finally308 {309 if (checker.UsesParameter)310 {311 context.EngineSessionState.RemoveScope(context.EngineSessionState.CurrentScope);312 }313 }314 }315 316 /// <summary>317 /// Get using values as dictionary for the Foreach-Object parallel cmdlet.318 /// Ignore any using expressions that are associated with inner nested Foreach-Object parallel calls,319 /// since they are only effective in the nested call scope and not the current outer scope.320 /// </summary>321 /// <param name = "scriptBlock">Scriptblock to search.</param>322 /// <param name = "isTrustedInput">True when input is trusted.</param>323 /// <param name = "context">Execution context.</param>324 /// <returns>Dictionary of using variable map.</returns>325 internal static Dictionary<string, object> GetUsingValuesForEachParallel(326 ScriptBlock scriptBlock,327 bool isTrustedInput,328 ExecutionContext context)329 {330 // Using variables for Foreach-Object -Parallel use are restricted to be within the331 // Foreach-Object -Parallel call scope. This will filter the using variable map to variables332 // only within the current (outer) Foreach-Object -Parallel call scope.333 var usingAsts = UsingExpressionAstSearcher.FindAllUsingExpressions(scriptBlock.Ast).ToList();334 UsingExpressionAst usingAst = null;335 var usingValueMap = new Dictionary<string, object>(usingAsts.Count);336 Version oldStrictVersion = null;337 try338 {339 if (context != null)340 {341 oldStrictVersion = context.EngineSessionState.CurrentScope.StrictModeVersion;342 context.EngineSessionState.CurrentScope.StrictModeVersion = PSVersionInfo.PSVersion;343 }344 345 for (int i = 0; i < usingAsts.Count; ++i)346 {347 usingAst = (UsingExpressionAst)usingAsts[i];348 if (IsInForeachParallelCallingScope(scriptBlock.Ast, usingAst))349 {350 var value = Compiler.GetExpressionValue(usingAst.SubExpression, isTrustedInput, context);351 string usingAstKey = PsUtils.GetUsingExpressionKey(usingAst);352 usingValueMap.TryAdd(usingAstKey, value);353 }354 }355 }356 catch (RuntimeException rte)357 {358 if (rte.ErrorRecord.FullyQualifiedErrorId.Equals("VariableIsUndefined", StringComparison.Ordinal))359 {360 throw InterpreterError.NewInterpreterException(361 targetObject: null,362 exceptionType: typeof(RuntimeException),363 errorPosition: usingAst.Extent,364 resourceIdAndErrorId: "UsingVariableIsUndefined",365 resourceString: AutomationExceptions.UsingVariableIsUndefined,366 args: rte.ErrorRecord.TargetObject);367 }368 }369 finally370 {371 if (context != null)372 {373 context.EngineSessionState.CurrentScope.StrictModeVersion = oldStrictVersion;374 }375 }376 377 return usingValueMap;378 }379 380 // List of Foreach-Object command names and aliases.381 // TODO: Look into using SessionState.Internal.GetAliasTable() to find all user created aliases.382 // But update Alias command logic to maintain reverse table that lists all aliases mapping383 // to a single command definition, for performance.384 private static readonly string[] forEachNames = new string[]385 {386 "ForEach-Object",387 "foreach",388 "%"389 };390 391 private static bool FindForEachInCommand(CommandAst commandAst)392 {393 // Command name is always the first element in the CommandAst.394 // e.g., 'foreach -parallel {}'395 var commandNameElement = (commandAst.CommandElements.Count > 0) ? commandAst.CommandElements[0] : null;396 if (commandNameElement is StringConstantExpressionAst commandName)397 {398 bool found = false;399 foreach (var foreachName in forEachNames)400 {401 if (commandName.Value.Equals(foreachName, StringComparison.OrdinalIgnoreCase))402 {403 found = true;404 break;405 }406 }407 408 if (found)409 {410 // Verify this is foreach-object with parallel parameter set.411 var bindingResult = StaticParameterBinder.BindCommand(commandAst);412 if (bindingResult.BoundParameters.ContainsKey("Parallel"))413 {414 return true;415 }416 }417 }418 419 return false;420 }421 422 /// <summary>423 /// Walks the using Ast to verify it is used within a foreach-object -parallel command424 /// and parameter set scope, and not from within a nested foreach-object -parallel call.425 /// </summary>426 /// <param name="scriptblockAst">Scriptblock Ast containing this using Ast</param>427 /// <param name="usingAst">Using Ast to check.</param>428 /// <returns>True if using expression is in current call scope.</returns>429 private static bool IsInForeachParallelCallingScope(430 Ast scriptblockAst,431 UsingExpressionAst usingAst)432 {433 Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null.");434 435 /*436 Example:437 $Test1 = "Hello"438 1 | ForEach-Object -Parallel {439 $using:Test1440 $Test2 = "Goodbye"441 1 | ForEach-Object -Parallel {442 $using:Test1 # Invalid using scope443 $using:Test2 # Valid using scope444 }445 }446 */447 448 // Search up the parent Ast chain for 'Foreach-Object -Parallel' commands.449 Ast currentParent = usingAst.Parent;450 while (currentParent != scriptblockAst)451 {452 // Look for Foreach-Object outer commands453 if (currentParent is CommandAst commandAst &&454 FindForEachInCommand(commandAst))455 {456 // Using Ast is outside the invoking foreach scope.457 return false;458 }459 460 currentParent = currentParent.Parent;461 }462 463 return true;464 }465 466 /// <summary>467 /// Get using values in the dictionary form.468 /// </summary>469 internal static Dictionary<string, object> GetUsingValuesAsDictionary(ScriptBlock scriptBlock, bool isTrustedInput, ExecutionContext context, Dictionary<string, object> variables)470 {471 return GetUsingValues(scriptBlock.Ast, isTrustedInput, context, variables, false).Item1;472 }473 474 /// <summary>475 /// Get using values in the array form.476 /// </summary>477 internal static object[] GetUsingValuesAsArray(ScriptBlock scriptBlock, bool isTrustedInput, ExecutionContext context, Dictionary<string, object> variables)478 {479 return GetUsingValues(scriptBlock.Ast, isTrustedInput, context, variables, false).Item2;480 }481 482 /// <summary>483 /// Collect values for UsingExpressions, in the form of a dictionary and an array.484 /// - The dictionary form is used when the remote server is PSv5 and later version for handling UsingExpression in Invoke-Command/Start-Job485 /// - The array form is used when the remote server is PSv3 and PSv4 for handling UsingExpression in Invoke-Command.486 /// </summary>487 /// <remarks>488 /// We still keep the array-form using values because we want to avoid any breaking changes when running Invoke-Command489 /// targeting PSv3 or PSv4 remote end -- if UsingExpressions are all in the same scope, then we still pass an array of using490 /// values to the remote end; otherwise, we will handle UsingExpression as if the remote end is PSv2.491 /// </remarks>492 /// <returns>493 /// A tuple of the dictionary-form and the array-form using values.494 /// If the array-form using value is null, then there are UsingExpressions used in different scopes.495 /// </returns>496 private static Tuple<Dictionary<string, object>, object[]> GetUsingValues(497 Ast body,498 bool isTrustedInput,499 ExecutionContext context,500 Dictionary<string, object> variables,501 bool filterNonUsingVariables)502 {503 Diagnostics.Assert(context != null || variables != null, "can't retrieve variables with no context and no variables");504 505 var usingAsts = UsingExpressionAstSearcher.FindAllUsingExpressions(body).ToList();506 var usingValueArray = new object[usingAsts.Count];507 var usingValueMap = new Dictionary<string, object>(usingAsts.Count);508 HashSet<string> usingVarNames = (variables != null && filterNonUsingVariables) ? new HashSet<string>() : null;509 510 // Used to check if the PSv3/PSv4 way of handling UsingExpression can continue to be used.511 bool hasUsingExprInDifferentScope = false;512 ScriptBlockAst sbClosestToPreUsingExpr = null;513 514 UsingExpressionAst usingAst = null;515 Version oldStrictVersion = null;516 try517 {518 if (context != null)519 {520 oldStrictVersion = context.EngineSessionState.CurrentScope.StrictModeVersion;521 context.EngineSessionState.CurrentScope.StrictModeVersion = PSVersionInfo.PSVersion;522 }523 524 for (int i = 0; i < usingAsts.Count; ++i)525 {526 usingAst = (UsingExpressionAst)usingAsts[i];527 object value = null;528 529 // This happens only when GetUsingValues gets called outside the ScriptBlockToPowerShellConverter class530 if (!hasUsingExprInDifferentScope && HasUsingExpressionsInDifferentScopes(usingAst, body, ref sbClosestToPreUsingExpr))531 {532 // If there are UsingExpressions in different scopes, the array-form using values will not be useful533 // even if the remote end is PSv3 or PSv4, because the way we handle using expression in PSv3 and PSv4534 // doesn't support UsingExpression in different scopes. In this case, we will set the array-form using535 // value to be null before return.536 //537 // Note that this check only affect array-form using value. In PSv5, we change the way to handle UsingExpression538 // on both client and server sides. The dictionary-form using values is used and UsingExpression in different539 // scope is supported.540 hasUsingExprInDifferentScope = true;541 }542 543 if (variables != null)544 {545 if (usingAst.SubExpression is not VariableExpressionAst variableAst)546 {547 throw InterpreterError.NewInterpreterException(null, typeof(RuntimeException),548 usingAst.Extent, "CantGetUsingExpressionValueWithSpecifiedVariableDictionary", AutomationExceptions.CantGetUsingExpressionValueWithSpecifiedVariableDictionary, usingAst.Extent.Text);549 }550 551 string varName = variableAst.VariablePath.UserPath;552 if (varName != null && variables.TryGetValue(varName, out value) && usingVarNames != null)553 {554 usingVarNames.Add(varName);555 }556 }557 else558 {559 value = Compiler.GetExpressionValue(usingAst.SubExpression, isTrustedInput, context);560 }561 562 // Collect UsingExpression value as an array563 usingValueArray[i] = value;564 565 // Collect UsingExpression value as a dictionary566 string usingAstKey = PsUtils.GetUsingExpressionKey(usingAst);567 usingValueMap.TryAdd(usingAstKey, value);568 }569 }570 catch (RuntimeException rte)571 {572 if (rte.ErrorRecord.FullyQualifiedErrorId.Equals("VariableIsUndefined", StringComparison.Ordinal))573 {574 throw InterpreterError.NewInterpreterException(null, typeof(RuntimeException),575 usingAst.Extent, "UsingVariableIsUndefined", AutomationExceptions.UsingVariableIsUndefined, rte.ErrorRecord.TargetObject);576 }577 else if (rte.ErrorRecord.FullyQualifiedErrorId.Equals("CantGetUsingExpressionValueWithSpecifiedVariableDictionary", StringComparison.Ordinal))578 {579 throw;580 }581 }582 finally583 {584 if (context != null)585 {586 context.EngineSessionState.CurrentScope.StrictModeVersion = oldStrictVersion;587 }588 }589 590 if (usingVarNames != null)591 {592 string[] keys = variables.Keys.ToArray();593 foreach (string key in keys)594 {595 if (!usingVarNames.Contains(key))596 {597 variables.Remove(key);598 }599 }600 }601 602 if (hasUsingExprInDifferentScope)603 {604 usingValueArray = null;605 }606 607 return Tuple.Create(usingValueMap, usingValueArray);608 }609 610 /// <summary>611 /// Check if the given UsingExpression is in a different scope from the previous UsingExpression that we analyzed.612 /// </summary>613 /// <remarks>614 /// Note that the value of <paramref name="usingExpr"/> is retrieved by calling 'UsingExpressionAstSearcher.FindAllUsingExpressions'.615 /// So <paramref name="usingExpr"/> is guaranteed not inside a workflow.616 /// </remarks>617 /// <param name="usingExpr">The UsingExpression to analyze.</param>618 /// <param name="topLevelParent">The top level Ast, should be either ScriptBlockAst or FunctionDefinitionAst.</param>619 /// <param name="sbClosestToPreviousUsingExpr">The ScriptBlockAst that represents the scope of the previously analyzed UsingExpressions.</param>620 private static bool HasUsingExpressionsInDifferentScopes(UsingExpressionAst usingExpr, Ast topLevelParent, ref ScriptBlockAst sbClosestToPreviousUsingExpr)621 {622 Diagnostics.Assert(topLevelParent is ScriptBlockAst || topLevelParent is FunctionDefinitionAst,623 "the top level parent should be either a ScriptBlockAst or FunctionDefinitionAst");624 625 // Scan up the parents of a UsingExpression to check if it's in a nested function/filter/ScriptBlock626 Ast current = usingExpr;627 do628 {629 current = current.Parent;630 631 var sbAst = current as ScriptBlockAst;632 if (sbAst != null)633 {634 // We find the closest parent ScriptBlockAst of the current UsingExpression, which represents the scope635 // that the current UsingExpression is in.636 637 if (sbClosestToPreviousUsingExpr == null)638 {639 // The current UsingExpression is the first one to be analyzed.640 sbClosestToPreviousUsingExpr = sbAst;641 return false;642 }643 644 if (sbAst == sbClosestToPreviousUsingExpr)645 {646 // The current UsingExpression is in the same scope as the previous UsingExpression we analyzed.647 return false;648 }649 650 // The current UsingExpression is in a different scope from the previous UsingExpression we analyzed.651 return true;652 }653 654 var funcAst = current as FunctionDefinitionAst;655 if (funcAst != null)656 {657 // The parent chain of the current UsingExpression reaches a FunctionDefinitionAst, then the UsingExpression658 // must be in 'Parameters' property of this FunctionDefinitionAst.659 // In this case, the 'Body' of this FunctionDefinitionAst represents the scope that the UsingExpression is in.660 661 if (sbClosestToPreviousUsingExpr == null)662 {663 // The current UsingExpression is the first one to be analyzed.664 sbClosestToPreviousUsingExpr = funcAst.Body;665 return false;666 }667 668 if (funcAst.Body == sbClosestToPreviousUsingExpr)669 {670 // The current UsingExpression is in the same scope as the previous UsingExpression we analyzed.671 return false;672 }673 674 // The current UsingExpression is in a different scope from the previous UsingExpression we analyzed.675 return true;676 }677 } while (current != topLevelParent);678 679 Diagnostics.Assert(false, "Unreachable Code. Top level parent is eitehr ScriptBlockAst or FunctionDefinitionAst, so it should return within the loop for sure.");680 // I don't think it's reachable, but if it happens, just assume there are UsingExpressions in different scopes.681 return true;682 }683 684 private void ConvertPipeline(PipelineAst pipelineAst, bool isTrustedInput)685 {686 foreach (var command in pipelineAst.PipelineElements)687 {688 ConvertCommand((CommandAst)command, isTrustedInput);689 }690 }691 692 private void ConvertCommand(CommandAst commandAst, bool isTrustedInput)693 {694 // First need command name.695 var commandName = GetCommandName(commandAst.CommandElements[0], isTrustedInput);696 697 var command = new Command(commandName, isScript: false, useLocalScope: _createLocalScope);698 699 // Handle redirections, if any (there can really be just 0 or 1).700 if (commandAst.Redirections.Count > 0)701 {702 Diagnostics.Assert(commandAst.Redirections.Count == 1, "only 1 kind of redirection is supported");703 Diagnostics.Assert(commandAst.Redirections[0] is MergingRedirectionAst, "unexpected redirection type");704 705 PipelineResultTypes fromType;706 switch (commandAst.Redirections[0].FromStream)707 {708 case RedirectionStream.Error:709 fromType = PipelineResultTypes.Error;710 break;711 712 case RedirectionStream.Warning:713 fromType = PipelineResultTypes.Warning;714 break;715 716 case RedirectionStream.Verbose:717 fromType = PipelineResultTypes.Verbose;718 break;719 720 case RedirectionStream.Debug:721 fromType = PipelineResultTypes.Debug;722 break;723 724 case RedirectionStream.Information:725 fromType = PipelineResultTypes.Information;726 break;727 728 case RedirectionStream.All:729 fromType = PipelineResultTypes.All;730 break;731 732 default:733 // Default to Error->Output to be compatible with V2.734 fromType = PipelineResultTypes.Error;735 break;736 }737 738 command.MergeMyResults(fromType, toResult: PipelineResultTypes.Output);739 }740 741 _powershell.AddCommand(command);742 743 // Now the parameters and arguments.744 foreach (var ast in commandAst.CommandElements.Skip(1))745 {746 var exprAst = ast as ExpressionAst;747 if (exprAst != null)748 {749 VariableExpressionAst variableAst = null;750 751 var usingExprAst = ast as UsingExpressionAst;752 if (usingExprAst != null)753 {754 string usingAstKey = PsUtils.GetUsingExpressionKey(usingExprAst);755 object usingValue = _usingValueMap[usingAstKey];756 variableAst = usingExprAst.SubExpression as VariableExpressionAst;757 if (variableAst != null && variableAst.Splatted)758 {759 // Support the splatting of a dictionary760 var parameters = usingValue as System.Collections.IDictionary;761 if (parameters != null)762 {763 _powershell.AddParameters(parameters);764 }765 else766 {767 // Support the splatting of an array768 var arguments = usingValue as System.Collections.IEnumerable;769 if (arguments != null)770 {771 foreach (object argument in arguments)772 {773 _powershell.AddArgument(argument);774 }775 }776 else777 {778 // Splat the object directly.779 _powershell.AddArgument(usingValue);780 }781 }782 }783 else784 {785 _powershell.AddArgument(usingValue);786 }787 788 continue;789 }790 791 variableAst = ast as VariableExpressionAst;792 if (variableAst != null && variableAst.Splatted)793 {794 GetSplattedVariable(variableAst);795 }796 else797 {798 var constantExprAst = ast as ConstantExpressionAst;799 object argument;800 if (constantExprAst != null801 && (LanguagePrimitives.IsNumeric(LanguagePrimitives.GetTypeCode(constantExprAst.StaticType))802 || constantExprAst.StaticType == typeof(System.Numerics.BigInteger)))803 {804 var commandArgumentText = constantExprAst.Extent.Text;805 argument = constantExprAst.Value;806 if (!commandArgumentText.Equals(constantExprAst.Value.ToString(), StringComparison.Ordinal))807 {808 // The wrapped number will actually return a PSObject which could end holding a reference to809 // a typetable, making the object runspace specific. We should find a better way to avoid810 // any possibility of sharing problems, though this is unlikely to cause problems.811 argument = ParserOps.WrappedNumber(argument, commandArgumentText);812 }813 }814 else815 {816 if (!isTrustedInput)817 {818 try819 {820 argument = GetSafeValueVisitor.GetSafeValue(exprAst, _context, GetSafeValueVisitor.SafeValueContext.GetPowerShell);821 }822 catch (System.Exception)823 {824 throw new ScriptBlockToPowerShellNotSupportedException(825 "CantConvertWithDynamicExpression",826 null,827 AutomationExceptions.CantConvertWithDynamicExpression,828 exprAst.Extent.Text);829 }830 }831 else832 {833 argument = GetExpressionValue(exprAst, isTrustedInput);834 }835 }836 837 _powershell.AddArgument(argument);838 }839 }840 else841 {842 AddParameter((CommandParameterAst)ast, isTrustedInput);843 }844 }845 }846 847 private string GetCommandName(CommandElementAst commandNameAst, bool isTrustedInput)848 {849 var exprAst = commandNameAst as ExpressionAst;850 string commandName;851 if (exprAst != null)852 {853 var value = GetExpressionValue(exprAst, isTrustedInput);854 if (value == null)855 {856 ScriptBlockToPowerShellChecker.ThrowError(857 new ScriptBlockToPowerShellNotSupportedException(858 "CantConvertWithScriptBlockInvocation", null, AutomationExceptions.CantConvertWithScriptBlockInvocation),859 exprAst);860 }861 862 if (value is CommandInfo)863 {864 commandName = ((CommandInfo)value).Name;865 }866 else867 {868 commandName = value as string;869 }870 }871 else872 {873 // If this assertion fires, the command name is determined incorrectly.874 Diagnostics.Assert(commandNameAst is CommandParameterAst, "Unexpected element not handled correctly.");875 commandName = commandNameAst.Extent.Text;876 }877 878 if (string.IsNullOrWhiteSpace(commandName))879 {880 // TODO: could use a better error here881 throw new ScriptBlockToPowerShellNotSupportedException(882 "CantConvertWithScriptBlockInvocation",883 null,884 AutomationExceptions.CantConvertWithScriptBlockInvocation);885 }886 887 return commandName;888 }889 890 private void GetSplattedVariable(VariableExpressionAst variableAst)891 {892 if (_context == null)893 {894 throw new PSInvalidOperationException(AutomationExceptions.CantConvertScriptBlockWithNoContext);895 }896 897 // Process the contents of a splatted variable into the arguments for this898 // command. If the variable contains a hashtable, distribute the key/value pairs899 // If it's an enumerable, then distribute the values as $args and finally900 // if it's a scalar, then the effect is equivalent to $var901 object splattedValue = _context.GetVariableValue(variableAst.VariablePath);902 foreach (var splattedParameter in PipelineOps.Splat(splattedValue, variableAst))903 {904 CommandParameter publicParameter = CommandParameter.FromCommandParameterInternal(splattedParameter);905 _powershell.AddParameter(publicParameter);906 }907 }908 909 private object GetExpressionValue(ExpressionAst exprAst, bool isTrustedInput)910 {911 // be sure that there's a context at hand912 if (_context == null)913 {914 var rs = RunspaceFactory.CreateRunspace(InitialSessionState.Create());915 rs.Open();916 _context = rs.ExecutionContext;917 }918 919 if (!isTrustedInput) // if it's not trusted, call the safe value visitor920 {921 return GetSafeValueVisitor.GetSafeValue(exprAst, _context, GetSafeValueVisitor.SafeValueContext.GetPowerShell);922 }923 924 return Compiler.GetExpressionValue(exprAst, isTrustedInput, _context, _usingValueMap);925 }926 927 private void AddParameter(CommandParameterAst commandParameterAst, bool isTrustedInput)928 {929 string nameSuffix;930 object argument;931 if (commandParameterAst.Argument != null)932 {933 var arg = commandParameterAst.Argument;934 var errorPos = commandParameterAst.ErrorPosition;935 bool spaceAfterParameter = (errorPos.EndLineNumber != arg.Extent.StartLineNumber ||936 errorPos.EndColumnNumber != arg.Extent.StartColumnNumber);937 nameSuffix = spaceAfterParameter ? ": " : ":";938 939 argument = GetExpressionValue(commandParameterAst.Argument, isTrustedInput);940 }941 else942 {943 nameSuffix = string.Empty;944 argument = null;945 }946 947 // first character in parameter name must be a dash948 _powershell.AddParameter(949 string.Create(CultureInfo.InvariantCulture, $"-{commandParameterAst.ParameterName}{nameSuffix}"),950 argument);951 }952 }953}954 