Source/Classes/PSSerialize.ps1
using module .\..\..\..\ObjectGraphTools using namespace System.Management.Automation using namespace System.Management.Automation.Language using namespace System.Collections using namespace System.Collections.Generic Class PSSerialize { # hidden static [Dictionary[String,Bool]]$IsConstrainedType = [Dictionary[String,Bool]]::new() hidden static [Dictionary[String,Bool]]$HasStringConstructor = [Dictionary[String,Bool]]::new() hidden static [String]$AnySingleQuote = "'|$([char]0x2018)|$([char]0x2019)" # NoLanguage mode only hidden static [int]$MaxLeafLength = 48 hidden static [int]$MaxKeyLength = 12 hidden static [int]$MaxValueLength = 16 hidden static [int[]]$NoLanguageIndices = 0, 1, -1 hidden static [int[]]$NoLanguageItems = 0, 1, -1 hidden $_Object hidden [PSLanguageMode]$LanguageMode = 'Restricted' # "NoLanguage" will stringify the object for displaying (Use: PSStringify) hidden [Int]$ExpandDepth = [Int]::MaxValue hidden [Bool]$Explicit hidden [Bool]$FullTypeName hidden [bool]$HighFidelity hidden [String]$Indent = ' ' hidden [Bool]$ExpandSingleton # The dictionary below defines the round trip property. Unless the `-HighFidelity` switch is set, # the serialization will stop (even it concerns a `PSCollectionNode`) when the specific property # type is reached. # * An empty string will return the string representation of the object: `"<Object>"` # * Any other string will return the string representation of the object property: `"$(<Object>.<Property>)"` # * A ScriptBlock will be invoked and the result will be used for the object value hidden static $RoundTripProperty = @{ 'Microsoft.Management.Infrastructure.CimInstance' = '' 'Microsoft.Management.Infrastructure.CimSession' = 'ComputerName' 'Microsoft.PowerShell.Commands.ModuleSpecification' = 'Name' 'System.DateTime' = { $($Input).ToString('o') } 'System.DirectoryServices.DirectoryEntry' = 'Path' 'System.DirectoryServices.DirectorySearcher' = 'Filter' 'System.Globalization.CultureInfo' = 'Name' 'Microsoft.PowerShell.VistaCultureInfo' = 'Name' 'System.Management.Automation.AliasAttribute' = 'AliasNames' 'System.Management.Automation.ArgumentCompleterAttribute' = 'ScriptBlock' 'System.Management.Automation.ConfirmImpact' = '' 'System.Management.Automation.DSCResourceRunAsCredential' = '' 'System.Management.Automation.ExperimentAction' = '' 'System.Management.Automation.OutputTypeAttribute' = 'Type' 'System.Management.Automation.PSCredential' = { ,@($($Input).UserName, @("(""$($($Input).Password | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)')) } 'System.Management.Automation.PSListModifier' = 'Replace' 'System.Management.Automation.PSReference' = 'Value' 'System.Management.Automation.PSTypeNameAttribute' = 'PSTypeName' 'System.Management.Automation.RemotingCapability' = '' 'System.Management.Automation.ScriptBlock' = 'Ast' 'System.Management.Automation.SemanticVersion' = '' 'System.Management.Automation.ValidatePatternAttribute' = 'RegexPattern' 'System.Management.Automation.ValidateScriptAttribute' = 'ScriptBlock' 'System.Management.Automation.ValidateSetAttribute' = 'ValidValues' 'System.Management.Automation.WildcardPattern' = { $($Input).ToWql().Replace('%', '*').Replace('_', '?').Replace('[*]', '%').Replace('[?]', '_') } 'Microsoft.Management.Infrastructure.CimType' = '' 'System.Management.ManagementClass' = 'Path' 'System.Management.ManagementObject' = 'Path' 'System.Management.ManagementObjectSearcher' = { $($Input).Query.QueryString } 'System.Net.IPAddress' = 'IPAddressToString' 'System.Net.IPEndPoint' = { $($Input).Address.Address; $($Input).Port } 'System.Net.Mail.MailAddress' = 'Address' 'System.Net.NetworkInformation.PhysicalAddress' = '' 'System.Security.Cryptography.X509Certificates.X500DistinguishedName' = 'Name' 'System.Security.SecureString' = { ,[string[]]("(""$($Input | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)') } 'System.Text.RegularExpressions.Regex' = '' 'System.RuntimeType' = '' 'System.Uri' = 'OriginalString' 'System.Version' = '' 'System.Void' = $Null } hidden $StringBuilder hidden [Int]$Offset = 0 hidden [Int]$LineNumber = 1 PSSerialize($Object) { $this._Object = $Object } PSSerialize($Object, $LanguageMode) { $this._Object = $Object $this.LanguageMode = $LanguageMode } PSSerialize($Object, $LanguageMode, $ExpandDepth) { $this._Object = $Object $this.LanguageMode = $LanguageMode $this.ExpandDepth = $ExpandDepth } PSSerialize( $Object, $LanguageMode = 'Restricted', $ExpandDepth = [Int]::MaxValue, $Explicit = $False, $FullTypeName = $False, $HighFidelity = $False, $ExpandSingleton = $False, $Indent = ' ' ) { $this._Object = $Object $this.LanguageMode = $LanguageMode $this.ExpandDepth = $ExpandDepth $this.Explicit = $Explicit $this.FullTypeName = $FullTypeName $this.HighFidelity = $HighFidelity $this.ExpandSingleton = $ExpandSingleton $this.Indent = $Indent } hidden static [String[]]$Parameters = 'LanguageMode', 'Explicit', 'FullTypeName', 'HighFidelity', 'Indent', 'ExpandSingleton' PSSerialize($Object, [HashTable]$Parameters) { $this._Object = $Object foreach ($Name in $Parameters.get_Keys()) { # https://github.com/PowerShell/PowerShell/issues/13307 if ($Name -notin [PSSerialize]::Parameters) { Throw "Unknown parameter: $Name." } $this.GetType().GetProperty($Name).SetValue($this, $Parameters[$Name]) } } [String]Serialize($Object) { if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } if (-not ('ConstrainedLanguage', 'FullLanguage' -eq $this.LanguageMode)) { if ($this.FullTypeName) { Write-Warning 'The FullTypeName switch requires Constrained - or FullLanguage mode.' } if ($this.Explicit) { Write-Warning 'The Explicit switch requires Constrained - or FullLanguage mode.' } } if ($Object -is [PSNode]) { $Node = $Object } else { $Node = [PSNode]::ParseInput($Object) } $this.StringBuilder = [System.Text.StringBuilder]::new() $this.Stringify($Node) return $this.StringBuilder.ToString() } hidden Stringify([PSNode]$Node) { $Value = $Node.Value $IsSubNode = $this.StringBuilder.Length -ne 0 if ($Null -eq $Value) { $this.StringBuilder.Append('$Null') return } $Type = $Node.ValueType $TypeName = "$Type" $TypeInitializer = if ($Null -ne $Type -and ( $this.LanguageMode -eq 'Full' -or ( $this.LanguageMode -eq 'Constrained' -and [PSLanguageType]::IsConstrained($Type) -and ( $this.Explicit -or -not ( $Type.IsPrimitive -or $Value -is [String] -or $Value -is [Object[]] -or $Value -is [Hashtable] ) ) ) ) ) { if ($this.FullTypeName) { if ($Type.FullName -eq 'System.Management.Automation.PSCustomObject' ) { '[System.Management.Automation.PSObject]' } # https://github.com/PowerShell/PowerShell/issues/2295 else { "[$($Type.FullName)]" } } elseif ($TypeName -eq 'System.Object[]') { "[Array]" } elseif ($TypeName -eq 'System.Management.Automation.PSCustomObject') { "[PSCustomObject]" } elseif ($Type.Name -eq 'RuntimeType') { "[Type]" } else { "[$TypeName]" } } if ($TypeInitializer) { $this.StringBuilder.Append($TypeInitializer) } if ($Node -is [PSLeafNode] -or (-not $this.HighFidelity -and [PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName))) { $MaxLength = if ($IsSubNode) { [PSSerialize]::MaxValueLength } else { [PSSerialize]::MaxLeafLength } $Expression = if ([PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName)) { $Property = [PSSerialize]::RoundTripProperty[$Node.ValueType.FullName] if ($Null -eq $Property) { $Null } elseif ($Property -is [String]) { if ($Property) { ,$Value.$Property } else { "$Value" } } elseif ($Property -is [ScriptBlock] ) { Invoke-Command $Property -InputObject $Value } elseif ($Property -is [HashTable]) { if ($this.LanguageMode -eq 'Restricted') { $Null } else { @{} } } elseif ($Property -is [Array]) { @($Property.foreach{ $Value.$_ }) } else { Throw "Unknown round trip property type: $($Property.GetType())."} } elseif ($Type.IsPrimitive) { $Value } elseif (-not $Type.GetConstructors()) { "$TypeName" } elseif ($Type.GetMethod('ToString', [Type[]]@())) { $Value.ToString() } elseif ($Value -is [Collections.ICollection]) { ,$Value } else { $Value } # Handle compression if ($Null -eq $Expression) { $Expression = '$Null' } elseif ($Expression -is [Bool]) { $Expression = "`$$Value" } elseif ($Expression -is [Char]) { $Expression = "'$Value'" } elseif ($Expression -is [ScriptBlock]) { $Expression = [Abbreviate]::new('{', $Expression, $MaxLength, '}') } elseif ($Expression -is [HashTable]) { $Expression = '@{}' } elseif ($Expression -is [Array]) { if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [Abbreviate]::new('[', $Expression[0], $MaxLength, ']') } else { $Space = if ($this.ExpandDepth -ge 0) { ' ' } $New = if ($TypeInitializer) { '::new(' } else { '@(' } $Expression = $New + ($Expression.foreach{ if ($Null -eq $_) { '$Null' } elseif ($_.GetType().IsPrimitive) { "$_" } elseif ($_ -is [Array]) { $_ -Join $Space } else { "'$_'" } } -Join ",$Space") + ')' } } elseif ($Type -and $Type.IsPrimitive) { if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [CommandColor]([String]$Expression[0]) } } else { if ($Expression -isnot [String]) { $Expression = "$Expression" } if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [StringColor]([Abbreviate]::new("'", $Expression, $MaxLength, "'")) } else { if ($Expression.Contains("`n")) { $Expression = "@'" + [Environment]::NewLine + "$Expression".Replace("'", "''") + [Environment]::NewLine + "'@" } else { $Expression = "'$($Expression -Replace [PSSerialize]::AnySingleQuote, '$0$0')'" } } } $this.StringBuilder.Append($Expression) } elseif ($Node -is [PSListNode]) { $ChildNodes = $Node.get_ChildNodes() $this.StringBuilder.Append('@(') if ($this.LanguageMode -eq 'NoLanguage') { if ($ChildNodes.Count -eq 0) { } elseif ($IsSubNode) { $this.StringBuilder.Append([Abbreviate]::Ellipses) } else { $Indices = [PSSerialize]::NoLanguageIndices if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } $LastIndex = $Null foreach ($Index in $Indices) { if ($Null -ne $LastIndex) { $this.StringBuilder.Append(',') } if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses),") } $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) $LastIndex = $Index } } } else { $this.Offset++ $StartLine = $this.LineNumber $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -isnot [PSLeafNode]) foreach ($ChildNode in $ChildNodes) { if ($ChildNode.Name -gt 0) { $this.StringBuilder.Append(',') $this.NewWord() } elseif ($ExpandSingle) { $this.NewWord('') } $this.Stringify($ChildNode) } $this.Offset-- if ($this.LineNumber -gt $StartLine) { $this.NewWord('') } } $this.StringBuilder.Append(')') } else { # if ($Node -is [PSMapNode]) { $ChildNodes = $Node.get_ChildNodes() if ($ChildNodes) { $this.StringBuilder.Append('@{') if ($this.LanguageMode -eq 'NoLanguage') { if ($ChildNodes.Count -gt 0) { $Indices = [PSSerialize]::NoLanguageItems if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } $LastIndex = $Null foreach ($Index in $Indices) { if ($IsSubNode -and $Index) { $this.StringBuilder.Append(";$([Abbreviate]::Ellipses)"); break } if ($Null -ne $LastIndex) { $this.StringBuilder.Append(';') } if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses);") } $this.StringBuilder.Append([VariableColor]( [PSKeyExpression]::new($ChildNodes[$Index].Name, [PSSerialize]::MaxKeyLength))) $this.StringBuilder.Append('=') if (-not $IsSubNode -or $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength) { $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) } else { $this.StringBuilder.Append([Abbreviate]::Ellipses) } $LastIndex = $Index } } } else { $this.Offset++ $StartLine = $this.LineNumber $Index = 0 $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or $ChildNodes[0] -isnot [PSLeafNode] $ChildNodes.foreach{ if ($Index++) { $Separator = if ($this.ExpandDepth -ge 0) { '; ' } else { ';' } $this.NewWord($Separator) } elseif ($this.ExpandDepth -ge 0) { if ($ExpandSingle) { $this.NewWord() } else { $this.StringBuilder.Append(' ') } } $this.StringBuilder.Append([PSKeyExpression]::new($_.Name, $this.LanguageMode, ($this.ExpandDepth -lt 0))) if ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' = ') } else { $this.StringBuilder.Append('=') } $this.Stringify($_) } $this.Offset-- if ($this.LineNumber -gt $StartLine) { $this.NewWord() } elseif ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' ') } } $this.StringBuilder.Append('}') } elseif ($Node -is [PSObjectNode] -and $TypeInitializer) { $this.StringBuilder.Append('::new()') } else { $this.StringBuilder.Append('@{}') } } } hidden NewWord() { $this.NewWord(' ') } hidden NewWord([String]$Separator) { if ($this.Offset -le $this.ExpandDepth) { $this.StringBuilder.AppendLine() for($i = $this.Offset; $i -gt 0; $i--) { $this.StringBuilder.Append($this.Indent) } $this.LineNumber++ } else { $this.StringBuilder.Append($Separator) } } [String] ToString() { if ($this._Object -is [PSNode]) { $Node = $this._Object } else { $Node = [PSNode]::ParseInput($this._Object) } $this.StringBuilder = [System.Text.StringBuilder]::new() $this.Stringify($Node) return $this.StringBuilder.ToString() } } |