Modules/DSCParser.psm1

function ConvertTo-DSCObject {
    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param
    (
        [Parameter(Mandatory = $true,
            ParameterSetName = 'Path')]
        [ValidateScript({
                if (-Not ($_ | Test-Path) ) {
                    throw "File or folder does not exist"
                }
                if (-Not ($_ | Test-Path -PathType Leaf) ) {
                    throw "The Path argument must be a file. Folder paths are not allowed."
                }
                return $true
            })]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'Content')]
        [System.String]
        $Content,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'Content')]
        [System.Boolean]
        $IncludeComments = $false
    )

    #region Variables
    $ParsedResults = @()
    #endregion

    # Define components we wish to filter out
    $noisyTypes = @(
        "StatementSeparator", "CommandArgument", "CommandParameter"
        if (-not $IncludeComments) {
            "Comment"
        }
    )

    $NoisyOperators = (",", "")
    # Define variable for handling parser errors
    $ParserErrors = $null

    # Tokenize the file's content to break it down into its various components;
    switch ($PsCmdlet.ParameterSetName) {
        'Path' {
            $parsedData = [System.Management.Automation.PSParser]::Tokenize((Get-Content $Path), [ref]$ParserErrors)
        }
        'Content' {
            $parsedData = [System.Management.Automation.PSParser]::Tokenize($Content, [ref]$ParserErrors)
        }
    }

    # Handle parser errors
    if ($null -ne $ParserErrors -and $ParserErrors.Count -gt 0) {
        ForEach ($ParserError in $ParserErrors) {
            switch ($ParserError.Message) {
                { $_ -like 'Could not find the module *' -or `
                        $_ -like 'Multiple versions of the module ''*'' were found*' } {
                    # The corresponding DSC object cannot be found because of a missing or duplicate module. Throw a terminating error
                    Throw ('ConvertTo-DSCObject: "{0}" (line {1}): {2}' -f $ParserError.Token.Content, $ParserError.Token.StartLine, $ParserError.Message)
                    break
                }
                default {
                    # unhandled/unknown error. Not sure whether the .token object contains useful content, assuming it does
                    Write-Warning ('ConvertTo-DSCObject: "{0}" (line {1}): {2}' -f $ParserError.Token.Content, $ParserError.Token.StartLine, $ParserError.Message)
                }
            }
        }
    }

    [array]$componentsArray = @()
    $currentValues = @()
    $nodeKeyWordEncountered = $false
    $ObjectsToClose = 0
    for ($i = 0; $i -lt $parsedData.Count; $i++) {
        if ($nodeKeyWordEncountered) {
            if ($parsedData[$i].Type -eq "GroupStart" -or $parsedData[$i].Content -eq '{') {
                $ObjectsToClose++
            } elseif (($parsedData[$i].Type -eq "GroupEnd" -or $parsedData[$i].Content -eq '}') -and $ObjectsToClose -gt 0) {
                $ObjectsToClose--
            }

            if ($parsedData[$i].Type -notin $noisyTypes -and $parsedData[$i].Content -notin $noisyParts) {
                $currentValues += $parsedData[$i]
                if ($parsedData[$i].Type -eq "GroupEnd" -and $parsedData[$i].Content -eq '}' -and $ObjectsToClose -eq 0) {
                    $componentsArray += , $currentValues
                    $currentValues = @()
                    $ObjectsToClose = 0
                }
            } elseif (($parsedData[$i].Type -eq "GroupEnd" -and $parsedData[$i - 2].Content -ne 'parameter' -and $parsedData[$i].Content -ne '}') -or
                ($parsedData[$i].Type -eq "GroupStart" -and $parsedData[$i - 1].Content -ne 'parameter' -and $parsedData[$i].Content -ne '{')) {
                $currentValues += $parsedData[$i]
            }
        } elseif ($parsedData[$i].Content -eq 'node') {
            $nodeKeyWordEncountered = $true
            $newIndexPosition = $i + 1
            while ($parsedData[$newIndexPosition].Type -ne 'Keyword' -and $i -lt $parsedData.Count) {
                $i++
                $newIndexPosition = $i + 1
            }
        }
    }

    $ParsedResults = $null
    if ($componentsArray.Count -gt 0) {
        $ParsedResults = Get-HashtableFromGroup -Groups $componentsArray -Path $Path -IncludeComments:$IncludeComments -NoisyOperators $NoisyOperators
    }
    return $ParsedResults
}

function Get-HashtableFromGroup {
    [CmdletBinding()]
    [OutputType([System.Collections.IDictionary])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Array]
        $Groups,

        [Parameter(Mandatory = $true)]
        [System.Array]
        $Path,

        [Parameter()]
        [System.Boolean]
        $IsSubGroup = $false,

        [Array] $NoisyOperators,

        [switch] $IncludeComments
    )

    # Loop through all the Resources identified within our configuration
    $currentIndex = 1
    $result = @()
    $ParsedResults = @()
    foreach ($group in $Groups) {
        $keywordFound = $false
        if (-not $IsSubGroup -and $currentIndex -le $Groups.Count - 2) {
            Write-Progress -PercentComplete ($currentIndex / ($Groups.Count - 2) * 100) -Activity "Parsing $Path [$($currentIndex)/$($Groups.Count-2)]"
        }
        $currentPropertyIndex = 0
        $currentProperty = ''
        while ($currentPropertyIndex -lt $group.Count) {
            $component = $group[$currentPropertyIndex]
            if ($component.Type -eq "Keyword" -and -not $keywordFound) {
                $result = [ordered] @{ ResourceName = $component.Content }
                $keywordFound = $true
            } elseif ($keywordFound) {
                # If the next component is a keyword and we've already identified a keyword for this group, that means that we are
                # looking at a CIMInstance property;
                if ($component.Type -eq "Keyword") {
                    $currentGroupEndFound = $false
                    $currentPosition = $currentPropertyIndex + 1
                    $subGroup = @($component)
                    $ObjectsToClose = 0
                    $allSubGroups = @()
                    [Array]$subResult = @()
                    while (!$currentGroupEndFound) {
                        $currentSubComponent = $group[$currentPosition]
                        if ($currentSubComponent.Type -eq 'GroupStart' -or $currentSubComponent.Content -eq '{') {
                            $ObjectsToClose++
                        } elseif ($currentSubComponent.Type -eq 'GroupEnd' -or $currentSubComponent.Content -eq '}') {
                            $ObjectsToClose--
                        }

                        $subGroup += $group[$currentPosition]
                        if ($ObjectsToClose -eq 0 -and $group[$currentPosition + 1].Type -ne 'Keyword' -and `
                                $group[$currentPosition].Type -ne 'Keyword') {
                            $currentGroupEndFound = $true
                        }

                        if ($ObjectsToClose -eq 0 -and $group[$currentPosition].Type -ne 'Keyword') {
                            $allSubGroups += , $subGroup
                            $subGroup = @()
                        }
                        $currentPosition++
                    }
                    $currentPropertyIndex = $currentPosition
                    $subResult = Get-HashtableFromGroup -Groups $allSubGroups -IsSubGroup $true -Path $Path -IncludeComments:$IncludeComments.IsPresent -NoisyOperators $NoisyOperators
                    $allSubGroups = @()
                    $subGroup = @()
                    $result.$currentProperty += $subResult
                }
                # If the next component is not an operator, that means that the current member is part of the previous property's
                # value;
                elseif ($group[$currentPropertyIndex + 1].Type -ne "Operator" -and $component.Content -ne "=" -and `
                        $component.Content -ne '{' -and $component.Content -ne '}' -and $group[$currentPropertyIndex + 1].Type -ne 'Keyword') {
                    switch ($component.Type) {
                        { $_ -in @("String", "Number") } {
                            $result.$currentProperty += $component.Content
                            break
                        }
                        { $_ -in @("Variable") } {
                            # Based on the logic if if it's TRUE or FALSE we keep it as a boolean
                            # if it's other type of variable we keep it as a string with added $ character
                            if ($component.Content.ToLower() -eq 'true') {
                                $result.$currentProperty += $true
                            } elseif ($component.Content.ToLower() -eq 'false') {
                                $result.$currentProperty += $false
                            } elseif ($component.Content.ToLower() -eq 'null') {
                                $result.$currentProperty += $null
                            } else {
                                $result.$currentProperty += "`$" + $component.Content
                            }
                            break
                        }
                        { $_ -in @("Member") } {
                            $result.$currentProperty += "." + $component.Content
                            break
                        }
                        { $_ -in @("Command") } {
                            $result.ResourceID += $component.Content
                            break
                        }
                        { $_ -in @("Comment") } {
                            if ($IncludeComments) {
                                $result.$("_metadata_" + $currentProperty) += $component.Content
                            }
                            break
                        }
                        { $_ -in @("GroupStart") } {

                            # Property is an Array
                            $result.$currentProperty = @()

                            do {
                                # we will need to wait till we find end of array rather than terminating the loop early on
                                $currentPropertyIndex++
                                while ($group[$currentPropertyIndex].Type -eq 'NewLine') {
                                    $currentPropertyIndex++
                                }

                                switch ($group[$currentPropertyIndex].Type) {
                                    # Property is an array of string or integer
                                    { $_ -in @("String", "Number", "Variable") } {
                                        do {
                                            $ValueToSet = $group[$currentPropertyIndex].Content
                                            if ($group[$currentPropertyIndex].Content -notin $noisyOperators) {
                                                $Type = $group[$currentPropertyIndex].Type
                                                if ($Type -eq "Variable") {
                                                    # Based on the logic if if it's TRUE or FALSE we keep it as a boolean
                                                    # if it's other type of variable we keep it as a string with added $ character

                                                    if ($ValueToSet.ToLower() -eq 'true') {
                                                        $ValueToSet = $true
                                                    } elseif ($ValueToSet.ToLower() -eq 'false') {
                                                        $ValueToSet = $false
                                                    } elseif ($ValueToSet.ToLower() -eq 'null') {
                                                        $ValueToSet = $null
                                                    } else {
                                                        $ValueToSet = "`$" + $ValueToSet
                                                        # Supports variable name escape syntax within arrays
                                                        Do {
                                                            $currentPropertyIndex++
                                                            $ValueToSet += $group[$CurrentPropertyIndex].Content
                                                        } until (($group[$CurrentPropertyIndex + 1].Type -eq 'Operator' -and $group[$CurrentPropertyIndex + 1].Content -eq ',') -or $group[$currentPropertyIndex + 1].Type -eq 'GroupEnd')
                                                    }
                                                }

                                                $result.$currentProperty += $ValueToSet
                                            }
                                            $currentPropertyIndex++
                                        }
                                        while ($group[$currentPropertyIndex].Type -ne 'GroupEnd')
                                        break
                                    }

                                    # Property is an array of CIMInstance
                                    "Keyword" {
                                        $CimInstanceComponents = @()
                                        $GroupsToClose = 0
                                        $FoundOneGroup = $false
                                        do {
                                            if ($group[$currentPropertyIndex].Type -eq 'GroupStart') {
                                                $FoundOneGroup = $true
                                                $GroupsToClose ++
                                            } elseif ($group[$currentPropertyIndex].Type -eq 'GroupEnd') {
                                                $GroupsToClose --
                                            }
                                            if ($group[$currentPropertyIndex].Content -notin $noisyOperators) {
                                                $CimInstanceComponents += $group[$currentPropertyIndex]
                                            }
                                            $currentPropertyIndex++
                                        }
                                        while ($group[$currentPropertyIndex - 1].Type -ne 'GroupEnd' -or $GroupsToClose -ne 0 -or -not $FoundOneGroup)
                                        $CimInstanceObject = Convert-CIMInstanceToPSObject -CIMInstance $CimInstanceComponents -NoisyOperators $NoisyOperators
                                        $result.$CurrentProperty += $CimInstanceObject
                                        break
                                    }
                                }

                            } while ($group[$currentPropertyIndex].Type -ne 'GroupEnd' -and $token.Content -ne ')')
                            break
                        }
                    }
                } elseif ($component.Content -notin $noisyOperators) {
                    switch ($component.Type) {
                        "Member" {
                            $currentProperty = $component.Content.ToString()

                            if (!$result.Contains($currentProperty)) {
                                $result.Add($currentProperty, $null)
                            }
                        }
                        "Variable" {
                            # This is added to handle advanced variables such as $Test.Nested.Variable on 1st level
                            $result.$currentProperty += "`$" + $component.Content
                            Do {
                                $currentPropertyIndex++
                                $result.$currentProperty += $group[$CurrentPropertyIndex].Content
                            } until ($group[$CurrentPropertyIndex + 1].Type -eq 'NewLine')
                        }
                    }
                }
            }
            $currentPropertyIndex++
        }

        if ($keywordFound) {
            $ParsedResults += $result
        }
        $currentIndex++
    }
    return $ParsedResults
}

function Convert-CIMInstanceToPSObject {
    [CmdletBinding()]
    [OutputType([System.Collections.IDictionary])]
    Param(
        [Parameter(Mandatory = $true)]
        [System.Object[]]
        $CIMInstance,

        [Array] $NoisyOperators
    )

    $result = [ordered] @{}
    $index = 0
    $CurrentMemberName = $null
    while ($index -lt $CimInstance.Count) {
        $token = $CIMInstance[$index]
        switch ($token.Type) {
            # The main token for the CIMInstance
            "Keyword" {
                $result.Add("CIMInstance", $token.Content)
                break
            }
            "Member" {
                $result.Add($token.Content, "")
                $CurrentMemberName = $token.Content
                break
            }
            { $_ -in "String", "Number" } {
                $result.$CurrentMemberName = $token.Content
                break
            }
            { $_ -in @("Variable") } {
                # Based on the logic if if it's TRUE or FALSE we keep it as a boolean
                # if it's other type of variable we keep it as a string with added $ character
                if ($token.Content.ToLower() -eq 'true') {
                    $result.$CurrentMemberName = $true
                } elseif ($token.Content.ToLower() -eq 'false') {
                    $result.$CurrentMemberName = $false
                } elseif ($token.Content.ToLower() -eq 'null') {
                    $result.$CurrentMemberName = $null
                } else {
                    $result.$CurrentMemberName = "`$" + $token.Content
                    # Supports variable name escape syntax within ciminstances
                    Do {
                        $index++
                        $result.$CurrentMemberName += $CIMInstance[$index].Content
                    } until ($CIMInstance[$index + 1].Type -eq 'NewLine')
                }
                break
            }
            { $_ -eq "GroupStart" -and $token.Content -eq '@(' } {
                $result.$CurrentMemberName = @()
                $index++
                while ($CimInstance[$index].Type -eq 'NewLine') {
                    $index++
                }
                switch ($CIMInstance[$index].Type) {
                    { $_ -in "String", "Number", "Variable" } {
                        $arrayContent = @()
                        do {
                            $content = $CIMInstance[$index].Content

                            if ($_ -in @("Variable")) {
                                # Based on the logic if if it's TRUE or FALSE we keep it as a boolean
                                # if it's other type of variable we keep it as a string with added $ character
                                if ($content.ToLower() -eq 'true') {
                                    $content = $true
                                } elseif ($content.ToLower() -eq 'false') {
                                    $content = $false
                                } elseif ($content.ToLower() -eq 'null') {
                                    $content = $null
                                } else {
                                    $content = "`$" + $content
                                }
                            } elseif ($_ -in @("String", "Number")) {
                                $content = $content
                            }
                            $arrayContent += $content
                            $index++
                        } while ($CIMInstance[$index].Type -ne 'GroupEnd')
                        $result.$CurrentMemberName = $arrayContent
                    }

                    # The Content of the Array is yet again CIMInstances. Recursively call into the current method;
                    "Keyword" {
                        while ($CIMInstance[$index].Content -ne ')' -and $CImInstance[$index].Type -ne 'GroupEnd') {
                            $CIMInstanceComponents = @()
                            $GroupsToClose = 0
                            $FoundOneGroup = $false
                            do {
                                if ($CIMInstance[$index].Type -eq 'GroupStart') {
                                    $FoundOneGroup = $true
                                    $GroupsToClose ++
                                } elseif ($CIMInstance[$index].Type -eq 'GroupEnd') {
                                    $GroupsToClose --
                                }
                                if ($CIMInstance[$index].Content -notin $noisyOperators) {
                                    $CimInstanceComponents += $CIMInstance[$index]
                                }
                                $index++
                            }
                            while ($CIMInstance[$index - 1].Type -ne 'GroupEnd' -or $GroupsToClose -ne 0 -or -not $FoundOneGroup)
                            $CimInstanceObject = Convert-CIMInstanceToPSObject -CIMInstance $CimInstanceComponents -NoisyOperators $NoisyOperators
                            $result.$CurrentMemberName += $CimInstanceObject
                            $index++
                        }
                    }
                }
            }
        }
        $index++
    }
    return $result
}