Private/Get-StaticColorScriptOutput.ps1
|
function ConvertTo-StaticColorScriptEvaluationResult { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [bool]$Success, [Parameter()] [AllowNull()] [object]$Value ) return [pscustomobject]@{ Success = $Success Value = $Value } } function Get-StaticExpandedStringVariableExpression { [CmdletBinding()] [OutputType([System.Management.Automation.Language.VariableExpressionAst])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ExpressionAst]$NestedExpression ) if ($NestedExpression -is [System.Management.Automation.Language.VariableExpressionAst]) { return [System.Management.Automation.Language.VariableExpressionAst]$NestedExpression } if ($NestedExpression -isnot [System.Management.Automation.Language.SubExpressionAst]) { return $null } $statements = @($NestedExpression.SubExpression.Statements) if ($statements.Count -ne 1) { return $null } $pipeline = $statements[0] -as [System.Management.Automation.Language.PipelineAst] if (-not $pipeline -or $pipeline.PipelineElements.Count -ne 1 -or $pipeline.Background) { return $null } $commandExpression = $pipeline.PipelineElements[0] -as [System.Management.Automation.Language.CommandExpressionAst] if (-not $commandExpression -or $commandExpression.Redirections.Count -ne 0) { return $null } return $commandExpression.Expression -as [System.Management.Automation.Language.VariableExpressionAst] } function Test-StaticExpandedStringTokenText { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][string]$TokenText, [Parameter(Mandatory)][string]$VariableName, [Parameter(Mandatory)][string]$VariableExtentText ) $simpleTokenText = '$' + $VariableName $bracedTokenText = '${' + $VariableName + '}' $subExpressionTokenText = '$(' + $VariableExtentText + ')' return $TokenText -ceq $simpleTokenText -or $TokenText -ceq $bracedTokenText -or $TokenText -ceq $subExpressionTokenText } function ConvertTo-StaticExpandedStringToken { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ExpressionAst]$NestedExpression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) $variableExpression = Get-StaticExpandedStringVariableExpression -NestedExpression $NestedExpression if (-not $variableExpression -or -not $variableExpression.VariablePath.IsUnqualified -or $variableExpression.Splatted) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $variableName = $variableExpression.VariablePath.UserPath $tokenText = $NestedExpression.Extent.Text $supportedTokenText = Test-StaticExpandedStringTokenText -TokenText $tokenText -VariableName $variableName -VariableExtentText $variableExpression.Extent.Text if ([string]::IsNullOrWhiteSpace($variableName) -or $variableName -notmatch '^[A-Za-z_][A-Za-z0-9_]*$' -or -not $supportedTokenText -or -not $Variables.ContainsKey($variableName)) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $variableValue = $Variables[$variableName] if ($variableValue -isnot [string] -and $variableValue -isnot [char]) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $hasExplicitBoundary = $tokenText.StartsWith('$(', [System.StringComparison]::Ordinal) -or $tokenText.StartsWith('${', [System.StringComparison]::Ordinal) $tokenBoundary = if ($hasExplicitBoundary) { '' } else { '(?![A-Za-z0-9_])' } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([pscustomobject]@{ Text = $tokenText Pattern = [regex]::Escape($tokenText) + $tokenBoundary VariableName = $variableName }) } function Get-StaticExpandedStringTokenPlan { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ExpandableStringExpressionAst]$Expression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) $tokenPatterns = [System.Collections.Generic.Dictionary[string, string]]::new( [System.StringComparer]::Ordinal) $replacementTokens = [System.Collections.Generic.List[string]]::new() $replacementVariableNames = [System.Collections.Generic.List[string]]::new() foreach ($nestedExpression in $Expression.NestedExpressions) { $tokenResult = ConvertTo-StaticExpandedStringToken -NestedExpression $nestedExpression -Variables $Variables if (-not $tokenResult.Success) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $token = $tokenResult.Value if (-not $tokenPatterns.ContainsKey($token.Text)) { $tokenPatterns.Add($token.Text, $token.Pattern) } $replacementTokens.Add($token.Text) $replacementVariableNames.Add($token.VariableName) } $orderedTokenTexts = @($tokenPatterns.Keys | Sort-Object -Property { $_.Length } -Descending) $orderedTokenPatterns = @($orderedTokenTexts | ForEach-Object { $tokenPatterns[$_] }) return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([pscustomobject]@{ Pattern = '(?:' + ($orderedTokenPatterns -join '|') + ')' Token = $replacementTokens VariableName = $replacementVariableNames }) } function ConvertFrom-StaticExpandedStringTemplate { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string]$Template, [Parameter(Mandatory)][object]$TokenPlan, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) $tokenRegex = [regex]::new( $TokenPlan.Pattern, [System.Text.RegularExpressions.RegexOptions]::CultureInvariant) $templateMatches = $tokenRegex.Matches($Template) if ($templateMatches.Count -ne $TokenPlan.Token.Count) { # If the decoded string contains an escaped occurrence of the same variable token, # source offsets no longer map to the decoded template. Fail closed instead of # guessing which occurrence PowerShell would expand. return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $builder = New-Object System.Text.StringBuilder $templateOffset = 0 for ($index = 0; $index -lt $templateMatches.Count; $index++) { $match = $templateMatches[$index] if ($match.Value -cne $TokenPlan.Token[$index]) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $null = $builder.Append($Template.Substring($templateOffset, $match.Index - $templateOffset)) $null = $builder.Append([string]$Variables[$TokenPlan.VariableName[$index]]) $templateOffset = $match.Index + $match.Length } $null = $builder.Append($Template.Substring($templateOffset)) return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value $builder.ToString() } function Resolve-StaticColorScriptExpandedString { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ExpandableStringExpressionAst]$Expression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) if ($Expression.NestedExpressions.Count -eq 0) { try { return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([string]$Expression.SafeGetValue()) } catch { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } } $tokenPlan = Get-StaticExpandedStringTokenPlan -Expression $Expression -Variables $Variables if (-not $tokenPlan.Success) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertFrom-StaticExpandedStringTemplate -Template ([string]$Expression.Value) -TokenPlan $tokenPlan.Value -Variables $Variables } function Resolve-StaticColorScriptVariableExpression { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.VariableExpressionAst]$Expression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) $variableName = $Expression.VariablePath.UserPath if (-not $Expression.VariablePath.IsUnqualified -or $Expression.Splatted -or [string]::IsNullOrWhiteSpace($variableName) -or $variableName -notmatch '^[A-Za-z_][A-Za-z0-9_]*$' -or -not $Variables.ContainsKey($variableName)) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value $Variables[$variableName] } function Resolve-StaticColorScriptCharacterExpression { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ConvertExpressionAst]$Expression ) $typeName = $Expression.Type.TypeName.FullName $allowedCharType = [string]::Equals($typeName, 'char', [System.StringComparison]::OrdinalIgnoreCase) -or [string]::Equals($typeName, 'System.Char', [System.StringComparison]::OrdinalIgnoreCase) $constant = $Expression.Child -as [System.Management.Automation.Language.ConstantExpressionAst] if (-not $allowedCharType -or -not $constant) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $numericTypes = @( [byte], [sbyte], [int16], [uint16], [int32], [uint32], [int64], [uint64]) if ($null -eq $constant.Value -or $constant.Value.GetType() -notin $numericTypes) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } try { $codePoint = [System.Convert]::ToInt64($constant.Value, [System.Globalization.CultureInfo]::InvariantCulture) if ($codePoint -lt [char]::MinValue -or $codePoint -gt [char]::MaxValue) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([char]$codePoint) } catch { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } } function Resolve-StaticColorScriptConcatenationExpression { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.BinaryExpressionAst]$Expression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) if ($Expression.Operator -ne [System.Management.Automation.Language.TokenKind]::Plus) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $left = Resolve-StaticColorScriptExpression -Expression $Expression.Left -Variables $Variables if (-not $left.Success -or $left.Value -isnot [string]) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $right = Resolve-StaticColorScriptExpression -Expression $Expression.Right -Variables $Variables if (-not $right.Success -or $right.Value -isnot [string]) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ($left.Value + $right.Value) } function Resolve-StaticColorScriptExpression { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ExpressionAst]$Expression, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) if ($Expression -is [System.Management.Automation.Language.StringConstantExpressionAst]) { return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([string]$Expression.Value) } if ($Expression -is [System.Management.Automation.Language.ExpandableStringExpressionAst]) { return Resolve-StaticColorScriptExpandedString -Expression $Expression -Variables $Variables } if ($Expression -is [System.Management.Automation.Language.VariableExpressionAst]) { return Resolve-StaticColorScriptVariableExpression -Expression $Expression -Variables $Variables } if ($Expression -is [System.Management.Automation.Language.ConvertExpressionAst]) { return Resolve-StaticColorScriptCharacterExpression -Expression $Expression } if ($Expression -is [System.Management.Automation.Language.BinaryExpressionAst]) { return Resolve-StaticColorScriptConcatenationExpression -Expression $Expression -Variables $Variables } return ConvertTo-StaticColorScriptEvaluationResult -Success $false } function Test-StaticColorScriptAst { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.ScriptBlockAst]$Ast ) return -not ( $Ast.ParamBlock -or $Ast.BeginBlock -or $Ast.ProcessBlock -or $Ast.DynamicParamBlock -or ($Ast.PSObject.Properties['CleanBlock'] -and $Ast.CleanBlock) -or $Ast.UsingStatements.Count -gt 0 -or $Ast.Attributes.Count -gt 0) } function Read-StaticColorScriptAst { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string]$ScriptPath ) # Windows PowerShell 5.1 treats UTF-8 files without a BOM as the active ANSI code page # when ParseFile is used. Read as UTF-8 explicitly so adjacent Unicode artwork cannot be # misparsed as part of a variable name. $source = [System.IO.File]::ReadAllText($ScriptPath, [System.Text.Encoding]::UTF8) $tokens = $null $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput( $source, $ScriptPath, [ref]$tokens, [ref]$parseErrors) if ($parseErrors.Count -gt 0 -or -not (Test-StaticColorScriptAst -Ast $ast)) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $statements = @($ast.EndBlock.Statements) if ($statements.Count -eq 0) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value $statements } function Resolve-StaticColorScriptAssignment { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.AssignmentStatementAst]$Statement, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables ) $variableExpression = $Statement.Left -as [System.Management.Automation.Language.VariableExpressionAst] if ($Statement.Operator -ne [System.Management.Automation.Language.TokenKind]::Equals -or -not $variableExpression -or -not $variableExpression.VariablePath.IsUnqualified -or $variableExpression.Splatted) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $variableName = $variableExpression.VariablePath.UserPath $right = $Statement.Right -as [System.Management.Automation.Language.CommandExpressionAst] if ([string]::IsNullOrWhiteSpace($variableName) -or $variableName -notmatch '^[A-Za-z_][A-Za-z0-9_]*$' -or $Variables.ContainsKey($variableName) -or -not $right -or $right.Redirections.Count -ne 0) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $evaluation = Resolve-StaticColorScriptExpression -Expression $right.Expression -Variables $Variables if (-not $evaluation.Success -or ($evaluation.Value -isnot [string] -and $evaluation.Value -isnot [char])) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([pscustomobject]@{ Name = $variableName Value = $evaluation.Value }) } function Get-StaticWriteHostCommand { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.StatementAst]$Statement ) $pipeline = $Statement -as [System.Management.Automation.Language.PipelineAst] if (-not $pipeline -or $pipeline.PipelineElements.Count -ne 1 -or $pipeline.Background) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $command = $pipeline.PipelineElements[0] -as [System.Management.Automation.Language.CommandAst] if (-not $command -or $command.InvocationOperator -ne [System.Management.Automation.Language.TokenKind]::Unknown -or -not [string]::Equals($command.GetCommandName(), 'Write-Host', [System.StringComparison]::OrdinalIgnoreCase) -or $command.Redirections.Count -ne 0) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value $command } function Resolve-StaticWriteHostArgument { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.CommandAst]$Command ) $argument = $null $noNewline = $false foreach ($element in @($Command.CommandElements | Select-Object -Skip 1)) { $parameter = $element -as [System.Management.Automation.Language.CommandParameterAst] if ($parameter) { if ($noNewline -or $parameter.Argument -or -not [string]::Equals($parameter.ParameterName, 'NoNewline', [System.StringComparison]::OrdinalIgnoreCase)) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $noNewline = $true continue } $expression = $element -as [System.Management.Automation.Language.ExpressionAst] if (-not $expression -or $argument) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } $argument = $expression } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value ([pscustomobject]@{ Argument = $argument NoNewline = $noNewline }) } function Add-StaticWriteHostOutput { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][object]$WriteHostArgument, [Parameter(Mandatory)] [System.Collections.Generic.IDictionary[string, object]]$Variables, [Parameter(Mandatory)] [System.Text.StringBuilder]$Output ) if ($WriteHostArgument.Argument) { $evaluation = Resolve-StaticColorScriptExpression -Expression $WriteHostArgument.Argument -Variables $Variables if (-not $evaluation.Success -or ($evaluation.Value -isnot [string] -and $evaluation.Value -isnot [char])) { return $false } $null = $Output.Append([string]$evaluation.Value) } if (-not $WriteHostArgument.NoNewline) { $null = $Output.Append([Environment]::NewLine) } return $true } function Invoke-StaticColorScriptStatement { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.StatementAst]$Statement, [Parameter(Mandatory)][object]$State ) if ($Statement -is [System.Management.Automation.Language.AssignmentStatementAst]) { if ($State.OutputStatementSeen) { return $false } $assignment = Resolve-StaticColorScriptAssignment -Statement $Statement -Variables $State.Variables if (-not $assignment.Success) { return $false } $State.Variables.Add($assignment.Value.Name, $assignment.Value.Value) return $true } $commandResult = Get-StaticWriteHostCommand -Statement $Statement if (-not $commandResult.Success) { return $false } $argumentResult = Resolve-StaticWriteHostArgument -Command $commandResult.Value if (-not $argumentResult.Success -or -not (Add-StaticWriteHostOutput -WriteHostArgument $argumentResult.Value -Variables $State.Variables -Output $State.Output)) { return $false } $State.OutputStatementSeen = $true return $true } function Invoke-StaticColorScriptStatementList { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [System.Management.Automation.Language.StatementAst[]]$Statement ) $state = [pscustomobject]@{ Variables = [System.Collections.Generic.Dictionary[string, object]]::new( [System.StringComparer]::OrdinalIgnoreCase) Output = New-Object System.Text.StringBuilder OutputStatementSeen = $false } foreach ($currentStatement in $Statement) { if (-not (Invoke-StaticColorScriptStatement -Statement $currentStatement -State $state)) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } } if (-not $state.OutputStatementSeen) { return ConvertTo-StaticColorScriptEvaluationResult -Success $false } return ConvertTo-StaticColorScriptEvaluationResult -Success $true -Value $state.Output.ToString() } function Get-StaticColorScriptOutput { <# .SYNOPSIS Extracts output from a colorscript that uses a provably static subset of PowerShell. .DESCRIPTION Parses the script without executing it and interprets only a deliberately narrow AST subset used by bundled static colorscripts: simple variable assignments, literal strings, [char] constants, previously assigned variable interpolation, string concatenation, and one or more Write-Host statements. Any command execution, member access, subexpression, control flow, redirection, scoped or unknown variable, nontrivial subexpression, unsupported cast, or other syntax fails closed so the caller retains isolated process execution. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string]$ScriptPath ) $notAvailable = [pscustomobject]@{ Available = $false Content = '' } if ([string]::IsNullOrWhiteSpace($ScriptPath) -or -not (Test-Path -LiteralPath $ScriptPath -PathType Leaf)) { return $notAvailable } try { $astResult = Read-StaticColorScriptAst -ScriptPath $ScriptPath if (-not $astResult.Success) { return $notAvailable } $interpretation = Invoke-StaticColorScriptStatementList -Statement @($astResult.Value) if (-not $interpretation.Success) { return $notAvailable } # Preserve the literal ANSI payload. The public rendering boundary is responsible for # removing it only when the caller explicitly requests -NoAnsiOutput. return [pscustomobject]@{ Available = $true Content = $interpretation.Value } } catch { Write-Verbose ("Static colorscript extraction failed for {0}: {1}" -f $ScriptPath, $_.Exception.Message) return $notAvailable } } |