Private/Read-NSPFortiGateConfigTree.ps1
|
function Read-NSPFortiGateConfigTree { <# .SYNOPSIS Builds the config/edit/set tree from FortiOS CLI output lines. .DESCRIPTION Returns the root sections. Each section has Path, Vdom, LineNumber, Settings, Entries, and Sections; each entry has Name, Vdom, LineNumber, Settings, and Sections. Settings values are string arrays. Console noise is tolerated: terminal wraps are rejoined and grep markers removed (Join-NSPFortiGateWrappedLine), prompt lines outside a block are skipped, and a quoted value spanning lines (a certificate, a multi-line comment) keeps its line breaks. #> [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]]$Line) $roots = [System.Collections.Generic.List[object]]::new() $stack = [System.Collections.Generic.Stack[object]]::new() $keywords = [System.Collections.Generic.HashSet[string]]::new([string[]]@('config', 'edit', 'set', 'unset', 'append', 'select', 'unselect', 'next', 'end')) $pending = $null $startLine = 0 # Write-Verbose is a cmdlet call even when verbose output is off; skip it unless asked for. $verbose = $VerbosePreference -ne 'SilentlyContinue' $logicalLines = @(Join-NSPFortiGateWrappedLine -Line $Line) # Every complete line tokenized up front, in one call ($null where a quote is left open); see # Split-NSPFortiGateLineBatch for why this isn't done line by line here. $texts = [string[]]::new($logicalLines.Count) for ($n = 0; $n -lt $logicalLines.Count; $n++) { $texts[$n] = $logicalLines[$n].Text } $prepared = Split-NSPFortiGateLineBatch -Text $texts for ($n = 0; $n -le $logicalLines.Count; $n++) { $logical = if ($n -lt $logicalLines.Count) { $logicalLines[$n] } else { $null } if ($null -ne $logical) { if ($null -ne $pending) { # Inside a multi-line quoted value (a certificate): only check whether this line closes it. # Re-walking the whole value on every line made long certificates quadratic. $text = $pending + "`n" + $logical.Text $closed = (Split-NSPFortiGateLineBatch -Text $text)[0] if ($null -eq $closed) { $pending = $text; continue } $parsed = @{ Tokens = $closed; Unterminated = $false } } else { $text = $logical.Text $startLine = $logical.LineNumber if ($null -ne $prepared[$n]) { $parsed = @{ Tokens = $prepared[$n]; Unterminated = $false } } elseif ($text -match '^\s*(set|append)\s') { # Only a set/append line may open a multi-line quoted value; a stray quote in a prompt must not swallow the file. $pending = $text continue } else { $parsed = Split-NSPFortiGateToken -Text $text } } } elseif ($null -ne $pending) { Write-Warning "Unterminated quoted value starting at line $startLine." $parsed = Split-NSPFortiGateToken -Text $pending } else { break } $pending = $null $tokens = $parsed.Tokens if ($tokens.Count -eq 0) { continue } $command = $tokens[0].ToLowerInvariant() $top = if ($stack.Count) { $stack.Peek() } else { $null } if (-not $keywords.Contains($command)) { if ($verbose -and $null -ne $top) { Write-Verbose "Skipped unrecognized line $startLine`: $($tokens -join ' ')" } continue } switch ($command) { 'config' { if ($tokens.Count -lt 2) { if ($verbose) { Write-Verbose "Skipped 'config' without a path at line $startLine." }; break } $path = ($tokens[1..($tokens.Count - 1)] -join ' ') $vdom = '' if ($null -ne $top) { $vdom = $top.Node.Vdom if ($top.Kind -eq 'Entry' -and $top.SectionPath -eq 'vdom') { $vdom = $top.Node.Name } elseif ($top.Kind -eq 'Section' -and $top.Node.Path -eq 'global') { $vdom = 'global' } } $section = [pscustomobject]@{ PSTypeName = 'NSP.FortiGate.ConfigSection' Path = $path Vdom = $vdom LineNumber = $startLine Settings = [ordered]@{} Entries = [System.Collections.Generic.List[object]]::new() Sections = [System.Collections.Generic.List[object]]::new() } if ($null -eq $top) { $roots.Add($section) } else { $top.Node.Sections.Add($section) } $stack.Push(@{ Kind = 'Section'; Node = $section }) } 'edit' { if ($null -eq $top -or $top.Kind -ne 'Section') { if ($verbose) { Write-Verbose "Skipped 'edit' outside a config block at line $startLine." }; break } $entry = [pscustomobject]@{ PSTypeName = 'NSP.FortiGate.ConfigEntry' Name = if ($tokens.Count -gt 1) { $tokens[1] } else { '' } Vdom = $top.Node.Vdom LineNumber = $startLine Settings = [ordered]@{} Sections = [System.Collections.Generic.List[object]]::new() } $top.Node.Entries.Add($entry) $stack.Push(@{ Kind = 'Entry'; Node = $entry; SectionPath = $top.Node.Path }) } 'next' { if ($null -ne $top -and $top.Kind -eq 'Entry') { [void]$stack.Pop() } } 'end' { if ($null -ne $top -and $top.Kind -eq 'Entry') { [void]$stack.Pop() } if ($stack.Count -and $stack.Peek().Kind -eq 'Section') { [void]$stack.Pop() } } default { if ($null -eq $top -or $tokens.Count -lt 2) { break } $settings = $top.Node.Settings $key = $tokens[1] $values = if ($tokens.Count -gt 2) { $tokens[2..($tokens.Count - 1)] } else { @() } # Only append/unselect need the current value. $existing = if ($command -in 'append', 'unselect' -and $settings.Contains($key)) { @($settings[$key]) } else { @() } switch ($command) { 'unset' { $settings[$key] = [string[]]@() } 'append' { $settings[$key] = [string[]]($existing + $values) } 'unselect' { $settings[$key] = [string[]]@($existing | Where-Object { $values -notcontains $_ }) } default { $settings[$key] = [string[]]$values } } } } } if ($stack.Count) { Write-Warning 'Input ended inside a config block without a matching end; output may be incomplete.' } , $roots.ToArray() } |