Datum.psm1
Class FileProvider { hidden $Path hidden [hashtable] $Store hidden [hashtable] $DatumHierarchyDefinition hidden [hashtable] $StoreOptions hidden [hashtable] $DatumHandlers FileProvider ($Path,$Store,$DatumHierarchyDefinition) { $this.Store = $Store $this.DatumHierarchyDefinition = $DatumHierarchyDefinition $this.StoreOptions = $Store.StoreOptions $this.Path = Get-Item $Path -ErrorAction SilentlyContinue $this.DatumHandlers = $DatumHierarchyDefinition.DatumHandlers $Result = Get-ChildItem $path | ForEach-Object { if($_.PSisContainer) { $val = [scriptblock]::Create("New-DatumFileProvider -Path `"$($_.FullName)`" -StoreOptions `$this.DataOptions -DatumHierarchyDefinition `$this.DatumHierarchyDefinition") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } else { $val = [scriptblock]::Create("Get-FileProviderData -Path `"$($_.FullName)`" -DatumHandlers `$this.DatumHandlers") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } } } } Class Node : hashtable { Node([hashtable]$NodeData) { $NodeData.keys | % { $This[$_] = $NodeData[$_] } $this | Add-member -MemberType ScriptProperty -Name Roles -Value { $PathArray = $ExecutionContext.InvokeCommand.InvokeScript('Get-PSCallStack')[2].Position.text -split '\.' $PropertyPath = $PathArray[2..($PathArray.count-1)] -join '\' Write-warning "Resolve $PropertyPath" $obj = [PSCustomObject]@{} $currentNode = $obj if($PathArray.Count -gt 3) { foreach ($property in $PathArray[2..($PathArray.count-2)]) { Write-Debug "Adding $Property property" $currentNode | Add-member -MemberType NoteProperty -Name $property -Value ([PSCustomObject]@{}) $currentNode = $currentNode.$property } } Write-Debug "Adding Resolved property to last object's property $($PathArray[-1])" $currentNode | Add-member -MemberType NoteProperty -Name $PathArray[-1] -Value ($PropertyPath) return $obj } } static ResolveDscProperty($Path) { "Resolve-DscProperty $Path" } } Class SecureDatum { [hashtable] hidden $UnprotectParams SecureDatum($Object,[hashtable]$UnprotectParams) { $this.UnprotectParams = $UnprotectParams if($Object -is [hashtable]) { $Object = [PSCustomObject]$Object } if ($Object -is [PSCustomObject]) { foreach ($Property in $Object.PSObject.Properties.name) { $MemberTypeParams = @{ MemberType = 'NoteProperty' Name = $Property Value = ([SecureDatum]::GetObject($Object.$Property,$UnprotectParams)) } if ($MemberTypeParams.Value -is [scriptblock]) { $MemberTypeParams.MemberType = 'ScriptProperty' } $This | Add-Member @MemberTypeParams } } } [string] ToString() { return "{$($this.PSObject.Properties.Name -join ', ')}" } static [object] GetObject($object,$UnprotectParams) { if($null -eq $object) { return $null } elseif($object -is [PSCustomObject] -or $object -is [hashtable]) { return ([SecureDatum]::new($object,$UnprotectParams)) } elseif ($object -is [System.Collections.IEnumerable] -and $object -isnot [string]) { $collection = @() $collection = foreach ($item in $object) { [SecureDatum]::GetObject($item,$UnprotectParams) } return $collection } elseif($object -is [string] -and $object -match "^\[ENC=[\w\W]*\]$") { $UnprotectScriptBlock = " `$Base64Data = `"$object`" `[SecureDatum]::Unprotect(`$Base64Data.Trim(),`$this.UnprotectParams) " return ([scriptblock]::Create($UnprotectScriptBlock)) } else { return $object } } static [object] Unprotect($object,$UnprotectParams) { return (Unprotect-Datum -Base64Data $object @UnprotectParams) } } function Compare-Hashtable { [CmdletBinding()] Param( $ReferenceHashtable, $DifferenceHashtable, [string[]] $Property = ($ReferenceHashtable.Keys + $DifferenceHashtable.Keys | Select-Object -Unique) ) Write-Debug "Compare-Hashtable -Ref @{$($ReferenceHashtable.keys -join ';') -Diff @{$($DifferenceHashtable.keys -join ';')} -Property [$($Property -join ', ')]" #Write-Debug "REF:`r`n$($ReferenceHashtable|ConvertTo-JSON)" #Write-Debug "DIFF:`r`n$($DifferenceHashtable|ConvertTo-JSON)" foreach ($PropertyName in $Property) { Write-debug " Testing <$PropertyName>'s value" if( ($inRef = $ReferenceHashtable.Contains($PropertyName)) -and ($inDiff = $DifferenceHashtable.Contains($PropertyName)) ) { if($ReferenceHashtable[$PropertyName] -as [hashtable[]] -or $DifferenceHashtable[$PropertyName] -as [hashtable[]] ) { if( (Compare-Hashtable -ReferenceHashtable $ReferenceHashtable[$PropertyName] -DifferenceHashtable $DifferenceHashtable[$PropertyName]) ) { Write-Debug " Skipping $PropertyName...." # If Compae returns something, they're not the same Continue } } else { Write-Debug "Comparing: $($ReferenceHashtable[$PropertyName]) With $($ReferenceHashtable[$PropertyName])" if($ReferenceHashtable[$PropertyName] -ne $DifferenceHashtable[$PropertyName]) { [PSCustomObject]@{ SideIndicator = '<=' PropertyName = $PropertyName Value = $ReferenceHashtable[$PropertyName] } [PSCustomObject]@{ SideIndicator = '=>' PropertyName = $PropertyName Value = $DifferenceHashtable[$PropertyName] } } } } else { Write-Debug " Property $PropertyName Not in one Side: Ref: [$($ReferenceHashtable.Keys -join ',')] | [$($DifferenceHashtable.Keys -join ',')]" if($inRef) { Write-Debug "$PropertyName found in Reference hashtable" [PSCustomObject]@{ SideIndicator = '<=' PropertyName = $PropertyName Value = $ReferenceHashtable[$PropertyName] } } else { Write-Debug "$PropertyName found in Difference hashtable" [PSCustomObject]@{ SideIndicator = '=>' PropertyName = $PropertyName Value = $DifferenceHashtable[$PropertyName] } } } } } function ConvertTo-Datum { param ( [Parameter(ValueFromPipeline)] $InputObject, [AllowNull()] $DatumHandlers = @{} ) process { if ($null -eq $InputObject) { return $null } # if There's a matching filter, process associated command and return result if($HandlerNames = [string[]]$DatumHandlers.Keys) { foreach ($Handler in $HandlerNames) { $FilterModule,$FilterName = $Handler -split '::' if(!(Get-Module $FilterModule)) { Import-Module $FilterModule -force -ErrorAction Stop } $FilterCommand = Get-Command -ErrorAction SilentlyContinue ("{0}\Test-{1}Filter" -f $FilterModule,$FilterName) if($FilterCommand -and ($InputObject | &$FilterCommand)) { try { if($ActionCommand = Get-Command -ErrorAction SilentlyContinue ("{0}\Invoke-{1}Action" -f $FilterModule,$FilterName)) { $ActionParams = @{} $CommandOptions = $Datumhandlers.$handler.CommandOptions.Keys # Populate the Command's params with what's in the Datum.yml, or from variables foreach( $ParamName in $ActionCommand.Parameters.keys ) { if( $ParamName -in $CommandOptions ) { $ActionParams.add($ParamName,$Datumhandlers.$handler.CommandOptions[$ParamName]) } elseif($ValueInScope = Get-Variable -name $ParamName -ErrorAction SilentlyContinue -ValueOnly ){ $ActionParams.add($ParamName,$ValueInScope) } } return (&$ActionCommand @ActionParams) } } catch { Write-Warning "Error using Datum Handler $Handler, returning Input Object" $InputObject } } } } if ($InputObject -is [System.Collections.Hashtable] -or ($InputObject -is [System.Collections.Specialized.OrderedDictionary])) { $hashKeys = [string[]]$InputObject.Keys foreach ($Key in $hashKeys) { $InputObject[$Key] = ConvertTo-Datum -InputObject $InputObject[$Key] -DatumHandlers $DatumHandlers } # Making the Ordered Dict Case Insensitive ([ordered]@{}+$InputObject) } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Datum -InputObject $object -DatumHandlers $DatumHandlers } ) Write-Output -NoEnumerate $collection } elseif ($InputObject -is [psobject]) { $hash = [ordered]@{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Datum -InputObject $property.Value -DatumHandlers $DatumHandlers } $hash } else { $InputObject } } } function ConvertTo-ProtectedDatum {###########ConvertTo-DatumSecureObjectReader param ( [Parameter(ValueFromPipeline)] $InputObject, $UnprotectOptions ) process { if ($UnprotectOptions.contains('ClearTextPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.ClearTextPassword | ConvertTo-SecureString -AsPlainText -force $null = $UnprotectOptions.remove('ClearTextPassword') } elseif ($UnprotectOptions.contains('SecureStringPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.SecureStringPassword | ConvertTo-SecureString $null = $UnprotectOptions.remove('SecureStringPassword') } [SecureDatum]::GetObject($InputObject,$UnprotectOptions) } } function Get-MergeStrategyFromString { [CmdletBinding()] [OutputType([hashtable])] param( [String] $MergeStrategy ) Write-Debug "Get-MergeStrategyFromString -MergeStrategy <$MergeStrategycls>" switch -regex ($MergeStrategy) { '^First$|^MostSpecific$' { @{ strategy = 'MostSpecific' } } '^Unique$|^ArrayUniques$' { @{ strategy = 'Unique' } } '^hash$|^MergeTopKeys$' { @{ strategy = 'hash' options = @{ knockout_prefix = '--' sort_merged_arrays = $false merge_basetype_arrays = $false #'MostSpecific' # or Unique merge_hash_arrays = @{ # $false #or Most Specific strategy = 'MostSpecificArray' #'MergeHashesByProperties' or 'UniqueByProperties' #PropertyNames = 'ObjectProperty1','objectProperty2' } } } } '^deep$|^MergeRecursively$' { @{ strategy = 'deep' options = @{ knockout_prefix = '--' sort_merged_arrays = $false merge_basetype_arrays = 'Unique' # or MostSpecific merge_hash_arrays = @{ # $false #or Most Specific strategy = 'MergeByPropertyTuple' # or 'Unique', or 'MostSpecific' PropertyNames = 'ObjectProperty1','objectProperty2' } } } } default { Write-Debug "Couldn't Match the strategy $MergeStrategy" @{ strategy = 'MostSpecific' } } } } function Merge-DatumArray { [CmdletBinding()] Param( $ReferenceArray, $DifferenceArray, $Strategy = @{ strategy = 'hash' options = @{ knockout_prefix = '--' sort_merged_arrays = $false merge_basetype_arrays = 'MostSpecific' # = $false, or Unique merge_hash_arrays = @{ # $false #or Strategy strategy = 'MostSpecific' #'ByPropertyTuple' or 'Unique' #PropertyNames = 'ObjectProperty1','objectProperty2' } } }, $ChildStrategies = @{'^.*' = $Strategy}, $StartingPath ) Write-Debug "`tMerge-DatumArray -StartingPath <$StartingPath>" $HashArrayStrategy = $Strategy.options.merge_hash_arrays.Strategy Write-Debug "`t`tHash Array Strategy: $HashArrayStrategy" $MergeBasetypeArraysStrategy = $Strategy.options.merge_basetype_arrays $MergedArray = [System.Collections.ArrayList]::new() $SortParams = @{} if($PropertyNames = [String[]]$Strategy.options.merge_hash_arrays.PropertyNames) { $SortParams.Add('Property',$PropertyNames) } if($ReferenceArray -as [hashtable[]]) { Write-Debug "`t`tMERGING Array of Hashtables" if(!$HashArrayStrategy -or $HashArrayStrategy -match 'MostSpecific') { Write-Debug "`t`tMerge_hash_arrays Disabled. value: $HashArrayStrategy" $MergedArray = $ReferenceArray if($Strategy.sort_merged_arrays) { $MergedArray = $MergedArray | Sort-Object @SortParams } return $MergedArray } switch -Regex ($HashArrayStrategy) { '^Sum|^Add' { (@($DifferenceArray) + @($ReferenceArray)).Foreach{ $null = $MergedArray.add(([ordered]@{}+$_)) } } # MergeHashesByProperties '^Merge' { Write-Debug "`t`t`tStrategy for Array Items: Merge Hash By tuple`r`n" # look at each $RefItems in $RefArray # if no PropertyNames defined, use all Properties of $RefItem # else use defined propertyNames # Search for DiffItem that has the same Property/Value pairs # if found, Merge-Datum (or MergeHashtable?) # if not found, add $DiffItem to $RefArray # look at each $RefItems in $RefArray $UsedDiffItems = [System.Collections.ArrayList]::new() foreach ($ReferenceItem in $ReferenceArray) { $ReferenceItem = [ordered]@{} + $ReferenceItem Write-Debug "`t`t`t .. Working on Merged Element $($MergedArray.Count)`r`n" # if no PropertyNames defined, use all Properties of $RefItem if(!$PropertyNames) { Write-Debug "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $PropertyNames = $ReferenceItem.Keys } $MergedItem = @{} + $ReferenceItem $DiffItemsToMerge = $DifferenceArray.Where{ $DifferenceItem = [ordered]@{} + $_ # Search for DiffItem that has the same Property/Value pairs than RefItem $CompareHashParams = @{ ReferenceHashtable = [ordered]@{}+$ReferenceItem DifferenceHashtable = $DifferenceItem Property = $PropertyNames } (!(Compare-Hashtable @CompareHashParams)) } Write-Debug "`t`t`t ..Items to merge: $($DiffItemsToMerge.Count)" $DiffItemsToMerge.Foreach{ $MergeItemsParams = @{ ParentPath = $StartingPath Strategy = $Strategy ReferenceHashtable = $MergedItem DifferenceHashtable = $_ ChildStrategies = $ChildStrategies } $MergedItem = Merge-Hashtable @MergeItemsParams } # If a diff Item has been used, save it to find the unused ones $null = $UsedDiffItems.AddRange($DiffItemsToMerge) $null = $MergedArray.Add($MergedItem) } $UnMergedItems = $DifferenceArray.Foreach{ if(!$UsedDiffItems.Contains($_)) { ([ordered]@{} + $_) } } $null = $MergedArray.AddRange($UnMergedItems) } # UniqueByProperties '^Unique' { Write-Debug "`t`t`tSelecting Unique Hashes accross both arrays based on Property tuples" # look at each $DiffItems in $DiffArray # if no PropertyNames defined, use all Properties of $DiffItem # else use defined PropertyNames # Search for a RefItem that has the same Property/Value pairs # if Nothing is found # add current DiffItem to RefArray if(!$PropertyNames) { Write-Debug "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $PropertyNames = $ReferenceItem.Keys } $MergedArray = [System.Collections.ArrayList]::new() $ReferenceArray.Foreach{ $CurrentRefItem = $_ if(!( $MergedArray.Where{!(Compare-Hashtable -Property $PropertyNames -ReferenceHashtable $CurrentRefItem -DifferenceHashtable $_ )})) { $null = $MergedArray.Add(([ordered]@{} +$_)) } } $DifferenceArray.Foreach{ $CurrentDiffItem = $_ if(!( $MergedArray.Where{!(Compare-Hashtable -Property $PropertyNames -ReferenceHashtable $CurrentDiffItem -DifferenceHashtable $_ )})) { $null = $MergedArray.Add(([ordered]@{} +$_)) } } } } } elseif(($ReferenceArray.Foreach{$_.getType()} | Select-Object -Unique).ToString() -eq 'System.Management.Automation.PSCustomObject') { Write-Debug "`t`tMERGING Arrays of PSObject" if(!$HashArrayStrategy -or $HashArrayStrategy -match 'MostSpecific') { Write-Debug "`t`tMerge_hash_arrays Disabled" $MergedArray = $ReferenceArray if($Strategy.sort_merged_arrays) { $MergedArray = $MergedArray | Sort-Object @SortParams } return $MergedArray } switch -Regex ($HashArrayStrategy) { # MergeHashesByProperties '^Merge' { Write-Debug "`t`t`tMerging Array of PSCustomObjects" # look at each $RefItems in $RefArray # if no PropertyNames defined, use all Properties of $RefItem # else use defined propertyNames # Search for DiffItem that has the same Property/Value pairs # if found, Merge-Datum (or MergeHashtable?) # if not found, add $DiffItem to $RefArray # foreach ($ReferenceItem in $ReferenceArray) { if(!$PropertyNames) { $PropertyNames = $ReferenceArray.PSObject.Properties.Name } throw "Not Implemented yet for PSObjects" } } # UniqueByProperties '^Unique' { Write-Debug "`t`t`tSelecting Unique PSCustomObjects accross both arrays" # look at each $DiffItems in $DiffArray # if no PropertyNames defined, use all Properties of $DiffItem # else use defined PropertyNames # Search for a RefItem that has the same Property/Value pairs # if Nothing is found # add current DiffItem to RefArray throw "Not Implemented yet for PSObjects" } } } else { Write-Debug "`t`tMERGING Arrays of basetype" if($MergeBasetypeArraysStrategy -eq 'Unique') { # TODO: knockout keys that match ^$knockout_prefix $MergedArray = ($ReferenceArray + $DifferenceArray).Foreach{$_} | Select-Object -Unique } elseif($MergeBasetypeArraysStrategy -match '^Sum|^add') { ($DifferenceArray + $ReferenceArray).Foreach{ $null = $MergedArray.add($_) } } else { Write-Debug "`t`tMerge_basetype_arrays Not relevant, Returning most specific" $MergedArray = $ReferenceArray } } if($Strategy.sort_merged_arrays) { $MergedArray = $MergedArray | Sort-Object @SortParams } $MergedArray } function Merge-Hashtable { [outputType([hashtable])] [cmdletBinding()] Param( # [hashtable] These should stay ordered $ReferenceHashtable, # [hashtable] These should stay ordered $DifferenceHashtable, [validateScript( { $_ -as [hashtable] -and $_.strategy -in @('hash','deep') -or $_ -in @('hash','deep') } )] $Strategy = @{ Strategy = 'deep' options = @{ knockoutprefix = '--' } }, $ChildStrategies = @{}, [string] $ParentPath ) Write-Debug "`tMerge-Hashtable -ParentPath <$ParentPath>" # Removing Case Sensitivity while keeping ordering $ReferenceHashtable = [ordered]@{} + $ReferenceHashtable $DifferenceHashtable = [ordered]@{} + $DifferenceHashtable $clonedReference = [ordered]@{} + $ReferenceHashtable if ($Strategy.options.knockout_prefix) { $KnockoutPrefix = $Strategy.options.knockout_prefix $KnockoutPrefixMatcher = [regex]::escape($KnockoutPrefix).insert(0,'^') } else { $KnockoutPrefixMatcher = [regex]::escape('--').insert(0,'^') } Write-Debug "`t Knockout Prefix Matcher: $knockoutPrefixMatcher" if($strategy -eq 'deep' -or $strategy.Strategy -eq 'deep') { $deepmerge = $true } else { $deepmerge = $false } $knockedOutKeys = $ReferenceHashtable.keys.where{$_ -match $KnockoutPrefixMatcher}.foreach{$_ -replace $KnockoutPrefixMatcher} Write-Debug "`t Knockedout Keys: [$($knockedOutKeys -join ', ')] from reference Hashtable Keys [$($ReferenceHashtable.keys -join ', ')]" foreach ($currentKey in $DifferenceHashtable.keys) { Write-Debug "`t CurrentKey: $currentKey" if($currentKey -in $knockedOutKeys) { Write-Debug "`t`tThe Key $currentkey is knocked out from the reference Hashtable." } elseif ($currentKey -match $KnockoutPrefixMatcher -and !$ReferenceHashtable.contains(($currentKey -replace $KnockoutPrefixMatcher))) { # it's a knockout coming from a lower level key, it should only apply down from here Write-Debug "`t`tKnockout prefix found for $currentKey in Difference hashtable, and key not set in Reference hashtable" if(!$ReferenceHashtable.contains($currentKey)) { Write-Debug "`t`t..adding knockout prefixed key for $curretKey to block further merges" $clonedReference.add($currentKey,$null) } } elseif (!$ReferenceHashtable.contains($currentKey) ) { #if the key does not exist in reference ht, create it using the DiffHt's value Write-Debug "`t Added Missing Key $currentKey of value: $($DifferenceHashtable[$currentKey]) from difference HT" $clonedReference.add($currentKey,$DifferenceHashtable[$currentKey]) } else { #the key exists, and it's not a knockout entry if (( $ReferenceHashtable[$currentKey] -is [System.Collections.IEnumerable] -and $ReferenceHashtable[$currentKey] -isnot [string]) ) { # both are hashtables and we're in Deepmerge mode $ChildPath = (Join-Path $ParentPath $currentKey) Write-Debug "`t .. Merging Arrays at current path $ChildPath`r`n$($Strategy|ConvertTo-Json)" $MergeDatumArrayParams = @{ StartingPath = $ChildPath Strategy = $Strategy ReferenceArray = $ReferenceHashtable[$currentKey] DifferenceArray = $DifferenceHashtable[$currentKey] ChildStrategies = $ChildStrategies } $subMerge = Merge-DatumArray @MergeDatumArrayParams $clonedReference[$currentKey] = $subMerge Write-Debug "`t .. Array Merged for path $ChildPath" } elseif ( $deepmerge -and ($ReferenceHashtable[$currentKey] -as [hashtable]) ) { # both are hashtables and we're in Deepmerge mode $ChildPath = (Join-Path $ParentPath $currentKey) Write-Debug "`t`t .. Merging Datums at current path $ChildPath" # if there's no Merge override for the subkey's path in the (not subkeys), # merge HASHTABLE with same strategy # otherwise, merge Datum $ChildStrategy = Get-MergeStrategyFromPath -Strategies $ChildStrategies -PropertyPath $ChildPath if($ChildStrategy.Default) { Write-Debug "`t`t ..Merging using the current Deep Strategy, Bypassing default" $MergePerDefault = @{ ParentPath = $ChildPath Strategy = $Strategy ReferenceHashtable = $ReferenceHashtable[$currentKey] DifferenceHashtable = $DifferenceHashtable[$currentKey] ChildStrategies = $ChildStrategies } $subMerge = Merge-Hashtable @MergePerDefault } else { Write-Debug "`t`t ..Merging using Override Strategy $($ChildStrategy|ConvertTo-Json)" $MergeDatumParam = @{ StartingPath = $ChildPath ReferenceDatum = $ReferenceHashtable[$currentKey] DifferenceDatum = $DifferenceHashtable[$currentKey] Strategies = $ChildStrategies } $subMerge = Merge-Datum @MergeDatumParam } Write-Debug "`t # Submerge $($submerge|ConvertTo-Json)." $clonedReference[$currentKey] = $subMerge } ####################### ---> add array merge and hashtable[] merge here (hashtable[] merge based on defined subkey) else { #one is not an hashtable or we're not in deepmerge mode, leave the ClonedReference as-is Write-debug "`t Using BaseType $($ReferenceHashtable[$currentKey].GetType()) from Ref[$currentkey] of value $($ReferenceHashtable[$currentKey])" #"; Diff[$currentkey] type $($DifferenceHashtable[$currentKey].GetType())" } } } return $clonedReference } <# $a = @{ keya = 1 keyb = 2 keyc = 3 '--keye' = $null } $b = @{ '--keya' = $null # removing keya keyb = 22 # won't override keyb keyd = 33 # will add keyd with value keye = 44 # keye should never be added, as it's removed from the ref ht } # simple merge: create keys from $b that do not exist in $a, remove --keys $d = [ordered]@{ a = [ordered]@{ x = 111 y = 222 z = 333 } b = 2 c = 3 d = 4 e = [ordered]@{ x = 111 '--y' = $null } } $c = @{ b = 0 #already defined, should ignore '--c' = $null #doesn't remove the key c from $c as it would violate the hierarchy #d missing intentionally, already defined e = @{ # key x omitted, already present y = 222 # this key 'y' should be added to $c.e z = 333 # this key 'z' should be added to $c.e } } $e = [ordered]@{ RootKey1 = [ordered]@{ subkey11 = [ordered]@{ subkey111 = 111 #'--Subkey112' = $null Subkey113 = 113 } subkey12 = [ordered]@{ subkey123 = 123 subkey124 = 124 } } RootKey2 = [ordered]@{ Subkey21 = [ordered]@{ Subkey211 = 211 Subkey212 = 212 Subkey213 = 213 } Subkey22 = @( 222 223 224 ) SubKey23 = @( [ordered]@{Name = 1; val1 = 1} [ordered]@{Name = 2; val1 = 2} [ordered]@{Name = 3; val1 = 3} ) } } $f = [ordered]@{ RootKey1 = [ordered]@{ subkey11 = [ordered]@{ subkey111 = 111 Subkey112 = 112 Subkey113 = 113 } subkey12 = [ordered]@{ subkey123 = 123 subkey124 = 124 } } RootKey2 = [ordered]@{ Subkey21 = [ordered]@{ Subkey211 = 2110 Subkey212 = 2120 Subkey213 = 2130 } Subkey22 = @( 221 ) SubKey23 = @( [ordered]@{Name = 1; val1 = 1} [ordered]@{Name = 2; val1 = 3} [ordered]@{Name = 3} ) } } $MergeParams = @{ StartingPath = 'root' ReferenceDatum = $e DifferenceDatum = $f Strategies = @{ 'root' = 'deep' 'root\rootkey2\Subkey22' = 'Unique' 'root\rootkey2\Subkey23' = 'Unique' '^.*' = 'deep' } } Merge-Datum @MergeParams #> function Get-FileProviderData { [CmdletBinding()] Param( $Path, [AllowNull()] $DatumHandlers = @{} ) Write-Verbose "Getting File Provider Data for Path: $Path" $File = Get-Item -Path $Path switch ($File.Extension) { '.psd1' { Import-PowerShellDataFile $File | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.json' { ConvertFrom-Json (Get-Content -Raw $Path) | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.yml' { ConvertFrom-Yaml (Get-Content -raw $Path) -ordered | ConvertTo-Datum -DatumHandlers $DatumHandlers } Default { Get-Content -Raw $Path } } } function Get-MergeStrategyFromPath { [CmdletBinding()] Param( $Strategies, $PropertyPath ) Write-debug "`tGet-MergeStrategyFromPath -PropertyPath <$PropertyPath> -Strategies $Strategies, count $($Strategies.count)" # Select Relevant strategy # Use exact path match first # or try Regex in order if ($Strategies.($PropertyPath)) { $StrategyKey = $PropertyPath Write-debug "`t Strategy found for exact key $StrategyKey" } elseif($Strategies.keys -and ($StrategyKey = [string]($Strategies.keys.where{$_.StartsWith('^') -and $_ -as [regex] -and $PropertyPath -match $_} | Select-Object -First 1)) ) { Write-debug "`t Strategy matching regex $StrategyKey" } else { Write-debug "`t No Strategy found" return #(Get-MergeStrategyFromString) } Write-Debug "`t StrategyKey: $StrategyKey" if( $Strategies[$StrategyKey] -is [string]) { Write-debug "`t Returning Strategy $StrategyKey from String '$($Strategies[$StrategyKey])'" Get-MergeStrategyFromString $Strategies[$StrategyKey] } else { Write-Debug "`t Returning Strategy $StrategyKey of type '$($Strategies[$StrategyKey].Strategy)'" $Strategies[$StrategyKey] } } function Invoke-ProtectedDatumAction { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string] $InputObject, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $PlainTextPassword, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) Write-Debug "Decrypting Datum using ProtectedData" $params = @{} foreach($ParamKey in $PSBoundParameters.keys) { if($ParamKey -in @('InputObject','PlainTextPassword')) { switch ($ParamKey) { 'PlainTextPassword' { $params.add('password',(ConvertTo-SecureString -AsPlainText -Force $PSBoundParameters[$ParamKey])) } 'InputObject' { $params.add('Base64Data',$InputObject) } } } else { $params.add($ParamKey,$PSBoundParameters[$ParamKey]) } } UnProtect-Datum @params } function Invoke-TestHandlerAction { Param( $Password, $test, $Datum ) @" Action: $handler Node: $($Node|FL *|Out-String) Params: $($PSBoundParameters | Convertto-Json) "@ } function Merge-Datum { [CmdletBinding()] param ( [string] $StartingPath, $ReferenceDatum, $DifferenceDatum, $Strategies = @{ '^.*' = 'MostSpecific' } ) Write-verbose "Merge-Datum -StartingPath <$StartingPath>" $strategies | ConvertTo-JSon -Depth 10 | Write-Verbose $Strategy = Get-MergeStrategyFromPath -Strategies $strategies -PropertyPath $startingPath -Verbose Write-Verbose " Strategy: $($Strategy.Strategy)" Write-Debug "---------------------------- $($Strategy | COnvertto-Json)" # Merge with strategy $mergeParams = @{ ReferenceHashtable = $ReferenceDatum DifferenceHashtable = $DifferenceDatum Strategy = $Strategy ParentPath = $StartingPath } switch ($Strategy.Strategy) { 'MostSpecific' { return $ReferenceDatum} 'AllValues' { return $DifferenceDatum } 'hash' { if($ReferenceDatum -isnot [string] -and $ReferenceDatum -is [System.Collections.IEnumerable]) { Write-Debug " HASH Merge: -> array of hashtable. Sending to Merge-DatumArray" if($DifferenceDatum -isnot [System.Collections.IEnumerable]) { $DifferenceDatum = @($DifferenceDatum) } $MergeDatumArrayParams = @{ ReferenceArray = $ReferenceDatum DifferenceArray = $DifferenceDatum Strategy = $Strategy ChildStrategies = $Strategies StartingPath = $StartingPath } Merge-DatumArray @MergeDatumArrayParams # it's an array of Hashtable, merge it by position, property, or uniqueness? } else { # ignore non-hashtable elements (replace with empty hash) if(!($ReferenceDatum -as [hashtable])) { $mergeParams['ReferenceHashtable'] = @{} } if(!($DifferenceDatum -as [hashtable])) { $mergeParams['DifferenceHashtable'] = @{} } # merge top layer keys, ignore subkeys Merge-Hashtable @mergeParams } } 'deep' { if($ReferenceDatum -is [hashtable] -or $ReferenceDatum -is [System.Collections.Specialized.OrderedDictionary]) { $mergeParams.Add('ChildStrategies',$Strategies) Write-Debug " Merging Hashtables" Merge-Hashtable @mergeParams } elseif($ReferenceDatum -isnot [string] -and $ReferenceDatum -is [System.Collections.IEnumerable]) { Write-Debug " DEEP Merge: -> array of objects. Sending to Merge-DatumArray" if($DifferenceDatum -isnot [System.Collections.IEnumerable]) { $DifferenceDatum = @($DifferenceDatum) } $MergeDatumArrayParams = @{ ReferenceArray = $ReferenceDatum DifferenceArray = $DifferenceDatum Strategy = $Strategy ChildStrategies = $Strategies StartingPath = $StartingPath } Merge-DatumArray @MergeDatumArrayParams } } } # Strategy is MostSpecific --> No Merge # strategy is All Values --> No Merge, return all # Strategy is Unique --> cast to refdatum [object[]] + diffDatum | select Unique # Strategy is Hash --> Merge Keys. # Strategy is Deep # --> is Array or [object[]]Value # --> Merge Hash[]? # ---> No, only keep refDatum # ---> Uniques: cast to refdatum [object[]] + diffDatum | select Unique # ---> ByKey: Merge ArrayItem.Where{$_.key -match refArrayItem.Key} # --> SubMode? Deep or hash # ---> ByPosition: Merge Ref[itemIndex] with Diff[itemIndex] # --> SubMode? Deep or hash # --> is Hash/Ordered } function New-DatumFileProvider { Param( [alias('DataOptions')] [AllowNull()] $Store, [AllowNull()] $DatumHierarchyDefinition = @{}, $Path = $Store.StoreOptions.Path ) if (!$DatumHierarchyDefinition) { $DatumHierarchyDefinition = @{} } [FileProvider]::new($Path, $Store,$DatumHierarchyDefinition) } function New-DatumStructure { [CmdletBinding( DefaultParameterSetName = 'FromConfigFile' )] Param ( [Parameter( Mandatory, ParameterSetName = 'DatumHierarchyDefinition' )] [Alias('Structure')] [hashtable] $DatumHierarchyDefinition, [Parameter( Mandatory, ParameterSetName = 'FromConfigFile' )] [io.fileInfo] $DefinitionFile ) switch ($PSCmdlet.ParameterSetName) { 'DatumHierarchyDefinition' { if ($DatumHierarchyDefinition.contains('DatumStructure')) { Write-debug "Loading Datum from Parameter" } elseif($DatumHierarchyDefinition.Path) { $DatumHierarchyFolder = $DatumHierarchyDefinition.Path Write-Debug "Loading default Datum from given path $DatumHierarchyFolder" } else { Write-Warning "Desperate attempt to load Datum from Invocation origin..." $CallStack = Get-PSCallstack $DatumHierarchyFolder = $CallStack[-1].psscritroot Write-Warning " ---> $DatumHierarchyFolder" } } 'FromConfigFile' { if((Test-Path $DefinitionFile)) { $DefinitionFile = (Get-Item $DefinitionFile -ErrorAction Stop) Write-Debug "File $DefinitionFile found. Loading..." $DatumHierarchyDefinition = Get-FileProviderData $DefinitionFile.FullName if(!$DatumHierarchyDefinition.contains('ResolutionPrecedence')) { Throw 'Invalid Datum Hierarchy Definition' } $DatumHierarchyFolder = $DefinitionFile.directory.FullName Write-Debug "Datum Hierachy Parent folder: $DatumHierarchyFolder" } else { Throw "Datum Hierarchy Configuration not found" } } } $root = @{} if($DatumHierarchyFolder -and !$DatumHierarchyDefinition.DatumStructure) { $Structures = foreach ($Store in (Get-ChildItem -Directory -Path $DatumHierarchyFolder)) { @{ StoreName = $Store.BaseName StoreProvider = 'Datum::File' StoreOptions = @{ Path = $Store.FullName } } } if($DatumHierarchyDefinition.contains('DatumStructure')) { $DatumHierarchyDefinition['DatumStructure'] = $Structures } else { $DatumHierarchyDefinition.add('DatumStructure',$Structures) } } # Define the default hierachy to be the StoreNames, when nothing is specified if ($DatumHierarchyFolder -and !$DatumHierarchyDefinition.ResolutionPrecedence) { if($DatumHierarchyDefinition.contains('ResolutionPrecedence')) { $DatumHierarchyDefinition['ResolutionPrecedence'] = $Structures.StoreName } else { $DatumHierarchyDefinition.add('ResolutionPrecedence',$Structures.StoreName) } } # Adding the Datum Definition to Root object $root.add('__Definition',$DatumHierarchyDefinition) foreach ($store in $DatumHierarchyDefinition.DatumStructure){ $StoreParams = @{ Store = (ConvertTo-Datum ([hashtable]$Store).clone()) Path = $store.StoreOptions.Path } # Accept Module Specification for Store Provider as String (unversioned) or Hashtable if($Store.StoreProvider -is [string]) { $StoreProviderModule, $StoreProviderName = $store.StoreProvider -split '::' } else { $StoreProviderModule = $Store.StoreProvider.ModuleName $StoreProviderName = $Store.StoreProvider.ProviderName if($Store.StoreProvider.ModuleVersion) { $StoreProviderModule = @{ ModuleName = $StoreProviderModule ModuleVersion = $Store.StoreProvider.ModuleVersion } } } if(!($Module = Get-Module $StoreProviderModule -ErrorAction SilentlyContinue)) { $Module = Import-Module $StoreProviderModule -Force -ErrorAction Stop -PassThru } $ModuleName = ($Module | Select-Object -First 1).Name $NewProvidercmd = Get-Command ("{0}\New-Datum{1}Provider" -f $ModuleName, $StoreProviderName) if( $StoreParams.Path -and ![io.path]::IsPathRooted($StoreParams.Path) -and $DatumHierarchyFolder ) { Write-Debug "Replacing Store Path with AbsolutePath" $StorePath = Join-Path $DatumHierarchyFolder $StoreParams.Path -Resolve -ErrorAction Stop $StoreParams['Path'] = $StorePath } if ($NewProvidercmd.Parameters.keys -contains 'DatumHierarchyDefinition') { Write-Debug "Adding DatumHierarchyDefinition to Store Params" $StoreParams.add('DatumHierarchyDefinition',$DatumHierarchyDefinition) } $storeObject = &$NewProvidercmd @StoreParams Write-Debug "Adding key $($store.storeName) to Datum root object" $root.Add($store.StoreName,$storeObject) } #return the Root Datum hashtable $root } #Requires -Modules ProtectedData function Protect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [PSObject] $InputObject, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Int] $MaxLineLength = 100, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { Write-Verbose "Deserializing the Object from Base64" $ProtectDataParams = @{ InputObject = $InputObject } Write-verbose "Calling Protect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $ProtectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $ProtectDataParams.Add('Password',$Password) } } $securedData = Protect-Data @ProtectDataParams $xml = [System.Management.Automation.PSSerializer]::Serialize($securedData, 5) $bytes = [System.Text.Encoding]::UTF8.GetBytes($xml) $Base64String = [System.Convert]::ToBase64String($bytes) if($MaxLineLength -gt 0) { $Base64DataBlock = [regex]::Replace($Base64String,"(.{$MaxLineLength})","`$1`r`n") } else { $Base64DataBlock = $Base64String } if(!$NoEncapsulation) { $Header,$Base64DataBlock,$Footer -join '' } else { $Base64DataBlock } } } Function Resolve-Datum { [cmdletBinding()] Param( [Parameter( Mandatory )] [string] $PropertyPath, [Parameter( Position = 1 )] [Alias('Node')] $Variable = $ExecutionContext.InvokeCommand.InvokeScript('$Node'), [string] $VariableName = 'Node', [Alias('DatumStructure')] $DatumTree = $ExecutionContext.InvokeCommand.InvokeScript('$ConfigurationData.Datum'), [Parameter( ParameterSetName = 'UseMergeOptions' )] [Alias('SearchBehavior')] $options, [string[]] [Alias('SearchPaths')] $PathPrefixes = $DatumTree.__Definition.ResolutionPrecedence, [int] $MaxDepth = $( if($MxdDpth = $DatumTree.__Definition.default_lookup_options.MaxDepth) { $MxdDpth } else { -1 }) ) # Manage lookup options: <# default_lookup_options Lookup_options options (argument) Behaviour MostSpecific for ^.* Present default_lookup_options + most Specific if not ^.* Present lookup_options + Default to most Specific if not ^.* Present options + Default to Most Specific if not ^.* Present Present Lookup_options + Default for ^.* if !Exists Present Present options + Default for ^.* if !Exists Present Present options override lookup options + Most Specific if !Exists Present Present Present options override lookup options + default for ^.* +========================+================+====================+============================================================+ | default_lookup_options | Lookup_options | options (argument) | Behaviour | +========================+================+====================+============================================================+ | | | | MostSpecific for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | | | default_lookup_options + most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | Present | | lookup_options + Default to most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | | Present | options + Default to Most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | | Lookup_options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | | Present | options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | | Present | Present | options override lookup options + Most Specific if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | Present | options override lookup options + default for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ If there's no default options, auto-add default options of mostSpecific merge, and tag as 'default' if there's a default options, use that strategy and tag as 'default' if the options implements ^.*, do not add Default_options, and do not tag 1. Defaults to Most Specific 2. Allow setting your own default, with precedence for non-default options 3. Overriding ^.* without tagging it as default (always match unless) #> Write-Debug "Resolve-Datum -PropertyPath <$PropertyPath> -Node $($Node.Name)" # Make options an ordered case insensitive variable if($options) { $options = [ordered]@{} + $options } if( !$DatumTree.__Definition.default_lookup_options ) { $default_options = Get-MergeStrategyFromString Write-Verbose " Default option not found in Datum Tree" } else { if($DatumTree.__Definition.default_lookup_options -is [string]) { $default_options = $(Get-MergeStrategyFromString -MergeStrategy $DatumTree.__Definition.default_lookup_options) } else { $default_options = $DatumTree.__Definition.default_lookup_options } #TODO: Add default_option input validation Write-Verbose " Found default options in Datum Tree of type $($default_options.Strategy)." } if( $DatumTree.__Definition.lookup_options) { Write-Debug " Lookup options found." $lookup_options = @{} + $DatumTree.__Definition.lookup_options } else { $lookup_options = @{} } # Transform options from string to strategy hashtable foreach ($optKey in ([string[]]$lookup_options.keys)) { if($lookup_options[$optKey] -is [string]) { $lookup_options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $lookup_options[$optKey] } } foreach ($optKey in ([string[]]$options.keys)) { if($options[$optKey] -is [string]) { $options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $options[$optKey] } } # using options if specified or lookup_options otherwise if (!$options) { $options = $lookup_options } # Add default strategy for ^.* if not present, at the end if(([string[]]$Options.keys) -notcontains '^.*') { # Adding Default flag $default_options.add('Default',$true) $options.add('^.*',$default_options) } # Create the variable to be used as Pivot in prefix path if( $Variable -and $VariableName ) { Set-Variable -Name $VariableName -Value $Variable -Force } # Scriptblock in path detection patterns $Pattern = '(?<opening><%=)(?<sb>.*?)(?<closure>%>)' $PropertySeparator = [IO.Path]::DirectorySeparatorChar $splitPattern = [regex]::Escape($PropertySeparator) $Depth = 0 $MergeResult = $null # Get the strategy for this path, to be used for merging $StartingMergeStrategy = Get-MergeStrategyFromPath -PropertyPath $PropertyPath -Strategies $options # Walk every search path in listed order, and return datum when found at end of path foreach ($SearchPrefix in $PathPrefixes) { #through the hierarchy $ArraySb = [System.Collections.ArrayList]@() $CurrentSearch = Join-Path $SearchPrefix $PropertyPath Write-Verbose '' Write-Verbose " Lookup <$CurrentSearch> $($Node.Name)" #extract script block for execution into array, replace by substition strings {0},{1}... $newSearch = [regex]::Replace($CurrentSearch, $Pattern, { param($match) $expr = $match.groups['sb'].value $index = $ArraySb.Add($expr) "`$({$index})" }, @('IgnoreCase', 'SingleLine', 'MultiLine')) $PathStack = $newSearch -split $splitPattern # Get value for this property path $DatumFound = Resolve-DatumPath -Node $Node -DatumTree $DatumTree -PathStack $PathStack -PathVariables $ArraySb Write-Debug " Depth: $depth; Merge options = $($options.count)" #Stop processing further path at first value in 'MostSpecific' mode (called 'first' in Puppet hiera) if ($DatumFound -and ($StartingMergeStrategy.Strategy -eq 'MostSpecific')) { return $DatumFound } elseif ( $DatumFound ) { if(!$MergeResult) { $MergeResult = $DatumFound } else { $MergeParams = @{ StartingPath = $PropertyPath ReferenceDatum = $MergeResult DifferenceDatum = $DatumFound Strategies = $options } $MergeResult = Merge-Datum @MergeParams } } #if we've reached the Maximum Depth allowed, return current result and stop further execution if ($Depth -eq $MaxDepth) { Write-Debug " Max depth of $MaxDepth reached. Stopping." return $MergeResult } } $MergeResult } function Resolve-DatumPath { [CmdletBinding()] param( [Alias('Variable')] $Node, [Alias('DatumStructure')] $DatumTree, [string[]] $PathStack, [System.Collections.ArrayList] $PathVariables ) $currentNode = $DatumTree $PropertySeparator = '.' #[io.path]::DirectorySeparatorChar $index = -1 Write-Debug "`t`t`t" foreach ($StackItem in $PathStack) { $index++ $RelativePath = $PathStack[0..$index] Write-Debug "`t`t`tCurrent Path: `$Datum$PropertySeparator$($RelativePath -join $PropertySeparator)" $RemainingStack = $PathStack[$index..($PathStack.Count-1)] Write-Debug "`t`t`t`tbranch of path Left to walk: $PropertySeparator$($RemainingStack[1..$RemainingStack.Length] -join $PropertySeparator)" if ( $StackItem -match '\{\d+\}') { Write-Debug -Message "`t`t`t`t`tReplacing expression $StackItem" $StackItem = [scriptblock]::Create( ($StackItem -f ([string[]]$PathVariables)) ).Invoke() Write-Debug -Message ($StackItem | Format-List * | Out-String) $PathItem = $stackItem } else { $PathItem = $CurrentNode.($ExecutionContext.InvokeCommand.ExpandString($StackItem)) } # if $PathItem is $null, it won't have subkeys, stop execution for this Prefix if($null -eq $PathItem) { Write-Verbose -Message " NULL FOUND at `$Datum.$($ExecutionContext.InvokeCommand.ExpandString(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables))`t`t <`$Datum$PropertySeparator$(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables)>" if($RemainingStack.Count -gt 1) { Write-Verbose -Message "`t`t----> before: $propertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($RemainingStack[1..($RemainingStack.Count-1)] -join $PropertySeparator)))`t`t <$(($RemainingStack[1..($RemainingStack.Count-1)] -join $PropertySeparator) -f [string[]]$PathVariables)>" } Return $null } else { $CurrentNode = $PathItem } if ($RemainingStack.Count -eq 1) { Write-Verbose -Message " VALUE found at `$Datum$PropertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables))" Write-Output $CurrentNode } } } function Test-ProtectedDatumFilter { Param( [Parameter( ValueFromPipeline )] $InputObject ) $InputObject -is [string] -and $InputObject.Trim() -match "^\[ENC=[\w\W]*\]$" } function Test-TestHandlerFilter { Param( [Parameter( ValueFromPipeline )] $inputObject ) $InputObject -is [string] -and $InputObject -match "^\[TEST=[\w\W]*\]$" } #Requires -Modules ProtectedData function Unprotect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string] $Base64Data, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { if (!$NoEncapsulation) { Write-Verbose "Removing $header DATA $footer " $Base64Data = $Base64Data -replace "^$([regex]::Escape($Header))" -replace "$([regex]::Escape($Footer))$" } Write-Verbose "Deserializing the Object from Base64" $bytes = [System.Convert]::FromBase64String($Base64Data) $xml = [System.Text.Encoding]::UTF8.GetString($bytes) $obj = [System.Management.Automation.PSSerializer]::Deserialize($xml) $UnprotectDataParams = @{ InputObject = $obj } Write-verbose "Calling Unprotect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $UnprotectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $UnprotectDataParams.Add('Password',$Password) } } Unprotect-Data @UnprotectDataParams } } |