functions/MergeDrivers/Merge-D365XmlFile.ps1

<#
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#>

#Requires -Version 7.0
<#
.SYNOPSIS
    Git merge driver for XML files whose repeated sibling elements are order-independent
    (for example D365FO metadata).
 
.DESCRIPTION
    Merge-driver mode (invoked automatically by git during a merge):
        1. Runs git's line-based 3-way merge (git merge-file). A clean result is kept as is.
        2. If that merge conflicts, sibling elements selected by the rules file are merged as
           an unordered collection: elements added on both sides are all kept (ours first,
           then theirs), deletions and one-sided changes are applied, and an element changed
           on both sides is left to the line-based merge. The line-based merge then runs again
           on the prepared versions. Only the text of the affected collections is rewritten;
           all other content (formatting, attribute layout, CDATA, line endings, BOM) is kept
           byte-for-byte.
        3. Remaining conflicts are written as standard git conflict markers and the script
           exits 1 so git marks the file as conflicted.
 
    The driver is not limited to D365FO: the rules decide which elements are order-independent.
 
    Rules file - one rule per line, lines starting with '#' are comments:
        <xpath> [key=<xpath>]
    <xpath> selects the order-independent elements. Element names are matched by local name,
    so rules work for namespaced documents without prefixes. The optional key is evaluated on
    each selected element and identifies it across base/ours/theirs (case-insensitive). Without a
    key the whole element is its identity. See D365FO.xmlmergerules for details and defaults.
 
    Rules file lookup (first match wins):
        -RulesPath, $env:D365GITOPS_XML_MERGE_RULES, <repository root>/.xmlmergerules,
        D365FO.xmlmergerules next to this script (defaults shipped with the module).
 
    Export mode (-ExportDefaultRules):
        Writes the shipped default rules to a file, e.g. to customize and commit them as
        <repository root>/.xmlmergerules.
 
    Register the merge driver once per clone (or in a pipeline step):
        git config merge.d365fo-xml.name "XML merger for order-independent elements"
        git config merge.d365fo-xml.driver "pwsh -NoProfile -File D365GitOps/functions/MergeDrivers/Merge-D365XmlFile.ps1 -Base %O -Ours %A -Theirs %B -MarkerSize %L -FilePath %P"
    and assign files to it in .gitattributes:
        *.xml merge=d365fo-xml
 
    Alternatively, install the D365GitOps module and run:
        Register-D365XmlFileMergeDriver
 
.EXAMPLE
    # Create a repository rules file from the shipped D365FO defaults:
    Merge-D365XmlFile -ExportDefaultRules ./.xmlmergerules
 
.EXAMPLE
    # Merge three versions the way git does; the result is written to ours.xml:
    pwsh -File D365GitOps/functions/MergeDrivers/Merge-D365XmlFile.ps1 -Base base.xml -Ours ours.xml -Theirs theirs.xml
#>

[CmdletBinding(DefaultParameterSetName = 'MergeDriver')]
param(
    # [Merge-driver] Ancestor (base) version of the file. Supplied as %O by git.
    [Parameter(Mandatory, ParameterSetName = 'MergeDriver')]
    [string]
    $Base
    ,
    # [Merge-driver] Current-branch version. Supplied as %A by git; the merged result is written here.
    [Parameter(Mandatory, ParameterSetName = 'MergeDriver')]
    [string]
    $Ours
    ,
    # [Merge-driver] Other-branch version. Supplied as %B by git.
    [Parameter(Mandatory, ParameterSetName = 'MergeDriver')]
    [string]
    $Theirs
    ,
    # [Merge-driver] Width of conflict-marker lines. Supplied as %L by git.
    [Parameter(ParameterSetName = 'MergeDriver')]
    [int]
    $MarkerSize = 7
    ,
    # [Merge-driver] Repository-relative path of the file. Supplied as %P by git; used in messages only.
    [Parameter(ParameterSetName = 'MergeDriver')]
    [string]
    $FilePath = ''
    ,
    # [Merge-driver] Rules file; takes precedence over $env:D365GITOPS_XML_MERGE_RULES and <repository root>/.xmlmergerules.
    [Parameter(ParameterSetName = 'MergeDriver')]
    [string]
    $RulesPath = ''
    ,
    # [Export] Path of the file that receives the default rules shipped with this script.
    [Parameter(Mandatory, ParameterSetName = 'ExportDefaultRules')]
    [string]
    $ExportDefaultRules
    ,
    # [Export] Overwrites an existing file.
    [Parameter(ParameterSetName = 'ExportDefaultRules')]
    [switch]
    $Force
)

if ($MyInvocation.InvocationName -eq '.') {
    return
}

Set-StrictMode -Version 1.0
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false

$defaultRulesPath = Join-Path $PSScriptRoot 'D365FO.xmlmergerules'

if ($PSCmdlet.ParameterSetName -eq 'ExportDefaultRules') {
    if ((Test-Path -LiteralPath $ExportDefaultRules) -and -not $Force) {
        throw "File '$ExportDefaultRules' already exists. Use -Force to overwrite it."
    }
    Copy-Item -LiteralPath $defaultRulesPath -Destination $ExportDefaultRules -Force
    return Get-Item -LiteralPath $ExportDefaultRules
}

$tagStopChars = [char[]]@('"', "'", '>')
$nameStopChars = [char[]]@(' ', "`t", "`r", "`n", '/', '>')
$disableFormatting = [System.Xml.Linq.SaveOptions]::DisableFormatting
$namespacedElements = [System.Xml.XPath.XPathExpression]::Compile("//*[namespace-uri() != '']")
$sideNames = 'Base', 'Ours', 'Theirs'

# ─── Files and rules ─────────────────────────────────────────────────────────

function Read-XmlTextFile {
    <#
    .SYNOPSIS
        Reads a file as strict UTF-8 and remembers whether it starts with a BOM.
    #>

    param(
        [string]
        $Path
    )

    $bytes = [System.IO.File]::ReadAllBytes($Path)
    $hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF
    $offset = if ($hasBom) { 3 } else { 0 }
    # Throws for non UTF-8 content (e.g. UTF-16), which keeps git's line-based result.
    $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($bytes, $offset, $bytes.Length - $offset)
    return [pscustomobject]@{ Text = $text; HasBom = $hasBom }
}

function Resolve-RulesFile {
    <#
    .SYNOPSIS
        Returns the rules file: parameter, environment variable, repository file or shipped defaults.
    #>

    $candidate = if (-not [string]::IsNullOrWhiteSpace($RulesPath)) {
        $RulesPath
    }
    elseif (-not [string]::IsNullOrWhiteSpace($env:D365GITOPS_XML_MERGE_RULES)) {
        $env:D365GITOPS_XML_MERGE_RULES
    }

    if ($candidate) {
        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
            throw "Rules file '$candidate' not found."
        }
        return (Resolve-Path -LiteralPath $candidate).ProviderPath
    }

    $repositoryRoot = & git rev-parse --show-toplevel 2>$null
    if ($LASTEXITCODE -eq 0 -and $repositoryRoot) {
        $repositoryRules = Join-Path $repositoryRoot '.xmlmergerules'
        if (Test-Path -LiteralPath $repositoryRules -PathType Leaf) {
            return (Resolve-Path -LiteralPath $repositoryRules).ProviderPath
        }
    }
    return $defaultRulesPath
}

function Read-XmlMergeRule {
    <#
    .SYNOPSIS
        Parses a rules file into rule objects (Index, Path, Key) with compiled XPath expressions.
    #>

    param(
        [string]
        $Path
    )

    $rules = [System.Collections.Generic.List[object]]::new()
    $lineNumber = 0
    foreach ($line in [System.IO.File]::ReadAllLines($Path)) {
        $lineNumber++
        $trimmed = $line.Trim()
        if ($trimmed.Length -eq 0 -or $trimmed.StartsWith('#')) {
            continue
        }
        $null = $trimmed -match '^(?<path>.+?)(?:\s+key\s*=\s*(?<key>.+))?$'
        $pathText = $Matches['path']
        $keyText = $Matches['key']
        try {
            $pathExpression = [System.Xml.XPath.XPathExpression]::Compile($pathText)
            $keyExpression = if ($keyText) { [System.Xml.XPath.XPathExpression]::Compile("string($keyText)") } else { $null }
        }
        catch {
            throw "Invalid rule in '$Path' line ${lineNumber}: $($_.Exception.Message)"
        }
        $rules.Add([pscustomobject]@{
                Index   = $rules.Count
                Path    = $pathExpression
                Key     = $keyExpression
                # Fast path for the common key=ChildElementName.
                KeyName = if ($keyText -match '^[\p{L}_][\p{L}\p{N}_.-]*$') { $keyText } else { $null }
            })
    }
    return , $rules
}

function New-XmlReaderSettings {
    <#
    .SYNOPSIS
        XML reader settings without DTD processing and external resolution (no XXE).
    #>

    $settings = [System.Xml.XmlReaderSettings]::new()
    $settings.DtdProcessing = [System.Xml.DtdProcessing]::Ignore
    $settings.XmlResolver = $null
    $settings.IgnoreWhitespace = $true
    return $settings
}

function Test-WellFormedXml {
    <#
    .SYNOPSIS
        Returns $true when the bytes are well-formed UTF-8 XML.
    #>

    param(
        [byte[]]
        $Bytes
    )

    try {
        $offset = if ($Bytes.Length -ge 3 -and $Bytes[0] -eq 0xEF -and $Bytes[1] -eq 0xBB -and $Bytes[2] -eq 0xBF) { 3 } else { 0 }
        $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($Bytes, $offset, $Bytes.Length - $offset)
        $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($text), (New-XmlReaderSettings))
        try {
            while ($reader.Read()) { }
        }
        finally {
            $reader.Dispose()
        }
        return $true
    }
    catch {
        return $false
    }
}

function Invoke-GitMergeFile {
    <#
    .SYNOPSIS
        Line-based 3-way merge via git merge-file; Conflicts is the git exit code (conflict count).
    #>

    param(
        [string]
        $OursPath
        ,
        [string]
        $BasePath
        ,
        [string]
        $TheirsPath
        ,
        [string]
        $ResultPath
    )

    Copy-Item -LiteralPath $OursPath -Destination $ResultPath -Force
    $null = & git merge-file "--marker-size=$MarkerSize" -L ours -L base -L theirs $ResultPath $BasePath $TheirsPath
    return [pscustomobject]@{
        Conflicts = $LASTEXITCODE
        Bytes     = [System.IO.File]::ReadAllBytes($ResultPath)
    }
}

# ─── XML model ───────────────────────────────────────────────────────────────

function ConvertTo-XmlModel {
    <#
    .SYNOPSIS
        Parses XML text into an XDocument with line info and namespace-free element names (for
        the XPath rules); element text spans are computed on demand by Get-ElementSpan.
    #>

    param(
        [string]
        $Text
    )

    $model = [pscustomobject]@{
        Text       = $Text
        Document   = $null
        LineBreaks = $null
        Spans      = [System.Collections.Generic.Dictionary[System.Xml.Linq.XElement, object]]::new()
    }
    if ([string]::IsNullOrWhiteSpace($Text)) {
        # Empty base of an add/add conflict.
        return $model
    }

    # Same line breaks as XmlReader line info; matches are computed natively and indexed on demand.
    $model.LineBreaks = [regex]::Matches($Text, '\r\n?|\n')

    $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($Text), (New-XmlReaderSettings))
    try {
        $model.Document = [System.Xml.Linq.XDocument]::Load($reader, [System.Xml.Linq.LoadOptions]::SetLineInfo)
    }
    finally {
        $reader.Dispose()
    }

    # Rules match local names: drop namespaces from element names in the in-memory copy.
    if ($Text.Contains('xmlns')) {
        $namespaced = [System.Collections.Generic.List[System.Xml.Linq.XElement]]::new()
        $iterator = [System.Xml.XPath.Extensions]::CreateNavigator($model.Document).Select($namespacedElements)
        while ($iterator.MoveNext()) {
            $namespaced.Add($iterator.Current.UnderlyingObject)
        }
        foreach ($element in $namespaced) {
            $element.Name = $element.Name.LocalName
            $declaration = $element.Attribute('xmlns')
            if ($null -ne $declaration) {
                $declaration.Remove()
            }
        }
    }
    return $model
}

function Get-NodeOffset {
    <#
    .SYNOPSIS
        Text offset of a node's line info position.
    #>

    param(
        $Model
        ,
        [System.Xml.IXmlLineInfo]
        $LineInfo
    )

    if ($LineInfo.LineNumber -le 1) {
        return $LineInfo.LinePosition - 1
    }
    $lineBreak = $Model.LineBreaks[$LineInfo.LineNumber - 2]
    return $lineBreak.Index + $lineBreak.Length + $LineInfo.LinePosition - 1
}

function Get-NodeStart {
    <#
    .SYNOPSIS
        Text offset of a node; line info points behind '<', '<!--', '<![CDATA[' or '<?'.
    #>

    param(
        $Model
        ,
        [System.Xml.Linq.XNode]
        $Node
    )

    $offset = Get-NodeOffset -Model $Model -LineInfo $Node
    if ($Node -is [System.Xml.Linq.XElement]) {
        return $offset - 1
    }
    if ($Node -is [System.Xml.Linq.XCData]) {
        return $offset - 9
    }
    if ($Node -is [System.Xml.Linq.XComment]) {
        return $offset - 4
    }
    if ($Node -is [System.Xml.Linq.XProcessingInstruction]) {
        return $offset - 2
    }
    return $offset
}

function Get-ElementSpan {
    <#
    .SYNOPSIS
        Text span of an element: Start, StartTagEnd, ContentEnd, End, IsEmpty, QualifiedName.
    #>

    param(
        $Model
        ,
        [System.Xml.Linq.XElement]
        $Element
    )

    $span = $null
    if ($Model.Spans.TryGetValue($Element, [ref]$span)) {
        return $span
    }

    $text = $Model.Text
    $start = Get-NodeStart -Model $Model -Node $Element
    # End of the start tag: first '>' outside quoted attribute values.
    $index = $start + 1
    while ($true) {
        $stop = $text.IndexOfAny($tagStopChars, $index)
        if ($stop -lt 0) {
            throw "Unterminated start tag at offset $start."
        }
        if ($text[$stop] -eq [char]'>') {
            break
        }
        $index = $text.IndexOf($text[$stop], $stop + 1) + 1
        if ($index -le 0) {
            throw "Unterminated attribute value at offset $stop."
        }
    }
    $startTagEnd = $stop + 1
    $isEmpty = $text[$stop - 1] -eq [char]'/'
    if ($isEmpty) {
        $contentEnd = $end = $startTagEnd
    }
    else {
        # The end tag closes right before the next node, or before the parent's end tag.
        $next = $Element.NextNode
        $limit = if ($null -ne $next) {
            Get-NodeStart -Model $Model -Node $next
        }
        elseif ($null -ne $Element.Parent) {
            (Get-ElementSpan -Model $Model -Element $Element.Parent).ContentEnd
        }
        else {
            $text.Length
        }
        $end = $text.LastIndexOf([char]'>', $limit - 1) + 1
        $contentEnd = $text.LastIndexOf('</', $end - 1, [System.StringComparison]::Ordinal)
    }
    $nameEnd = $text.IndexOfAny($nameStopChars, $start + 1)
    $span = [pscustomobject]@{
        Start         = $start
        StartTagEnd   = $startTagEnd
        ContentEnd    = $contentEnd
        End           = $end
        IsEmpty       = $isEmpty
        QualifiedName = $text.Substring($start + 1, $nameEnd - $start - 1)
    }
    $Model.Spans.Add($Element, $span)
    return $span
}

function Get-CollectionMember {
    <#
    .SYNOPSIS
        Identity keys of the elements selected by the rules (unique per parent) and their
        parents in document order.
    #>

    param(
        $Model
        ,
        $Rules
    )

    $memberKeys = [System.Collections.Generic.Dictionary[System.Xml.Linq.XElement, string]]::new()
    $parents = [System.Collections.Generic.List[System.Xml.Linq.XElement]]::new()
    $result = [pscustomobject]@{ Keys = $memberKeys; Parents = $parents }
    if ($null -eq $Model.Document) {
        return $result
    }

    $baseKeys = [System.Collections.Generic.Dictionary[System.Xml.Linq.XElement, string]]::new()
    $parentSet = [System.Collections.Generic.HashSet[System.Xml.Linq.XElement]]::new()
    $navigator = [System.Xml.XPath.Extensions]::CreateNavigator($Model.Document)
    foreach ($rule in $Rules) {
        $iterator = $navigator.Select($rule.Path)
        while ($iterator.MoveNext()) {
            $element = $iterator.Current.UnderlyingObject -as [System.Xml.Linq.XElement]
            if ($null -eq $element -or $null -eq $element.Parent -or $baseKeys.ContainsKey($element)) {
                continue
            }
            $keyValue = if ($null -ne $rule.KeyName) {
                $keyElement = $element.Element($rule.KeyName)
                if ($null -ne $keyElement) { $keyElement.Value }
            }
            elseif ($null -ne $rule.Key) {
                [string]([System.Xml.XPath.Extensions]::CreateNavigator($element).Evaluate($rule.Key))
            }
            $baseKey = if ($keyValue) {
                "k$($rule.Index):$($keyValue.ToUpperInvariant())"
            }
            else {
                "s$($rule.Index):$($element.ToString($disableFormatting))"
            }
            $baseKeys.Add($element, $baseKey)
            if ($parentSet.Add($element.Parent)) {
                $parents.Add($element.Parent)
            }
        }
    }

    foreach ($parent in $parents) {
        $occurrences = [System.Collections.Generic.Dictionary[string, int]]::new()
        foreach ($child in $parent.Elements()) {
            $baseKey = $null
            if ($baseKeys.TryGetValue($child, [ref]$baseKey)) {
                $count = 0
                $null = $occurrences.TryGetValue($baseKey, [ref]$count)
                $occurrences[$baseKey] = ++$count
                $memberKeys.Add($child, "$baseKey#$count")
            }
        }
    }
    $parents.Sort([System.Comparison[System.Xml.Linq.XElement]] { param($x, $y) [System.Xml.Linq.XNode]::CompareDocumentOrder($x, $y) })
    return $result
}

function Get-NodeStep {
    <#
    .SYNOPSIS
        Identity of one element below its parent: member key, else position and local name.
    #>

    param(
        $MemberKeys
        ,
        [System.Xml.Linq.XElement]
        $Element
    )

    $key = $null
    if ($MemberKeys.TryGetValue($Element, [ref]$key)) {
        return "k$key"
    }
    return "p$(@($Element.ElementsBeforeSelf($Element.Name)).Count + 1)|$($Element.Name.LocalName)"
}

function Get-IdentityStep {
    <#
    .SYNOPSIS
        Identity steps of an element from the document element down.
    #>

    param(
        $MemberKeys
        ,
        [System.Xml.Linq.XElement]
        $Element
    )

    $steps = [System.Collections.Generic.List[string]]::new()
    for ($current = $Element; $null -ne $current; $current = $current.Parent) {
        $steps.Insert(0, (Get-NodeStep -MemberKeys $MemberKeys -Element $current))
    }
    return , $steps
}

function Find-ByIdentityStep {
    <#
    .SYNOPSIS
        Element matching the identity steps taken from another version, or $null.
    #>

    param(
        $Model
        ,
        $MemberKeys
        ,
        $Steps
    )

    if ($null -eq $Model.Document -or (Get-NodeStep -MemberKeys $MemberKeys -Element $Model.Document.Root) -cne $Steps[0]) {
        return $null
    }
    $current = $Model.Document.Root
    for ($i = 1; $i -lt $Steps.Count; $i++) {
        $step = $Steps[$i]
        $next = $null
        if ($step[0] -eq [char]'p') {
            $separator = $step.IndexOf('|')
            $candidate = @($current.Elements($step.Substring($separator + 1)))[[int]$step.Substring(1, $separator - 1) - 1]
            if ($null -ne $candidate -and -not $MemberKeys.ContainsKey($candidate)) {
                $next = $candidate
            }
        }
        else {
            foreach ($child in $current.Elements()) {
                $key = $null
                if ($MemberKeys.TryGetValue($child, [ref]$key) -and "k$key" -ceq $step) {
                    $next = $child
                    break
                }
            }
        }
        if ($null -eq $next) {
            return $null
        }
        $current = $next
    }
    return $current
}

# ─── Unordered collection merge ──────────────────────────────────────────────

function Get-CollectionUnit {
    <#
    .SYNOPSIS
        Splits the content of a collection parent into units (leading whitespace/comments +
        member element) and the trailing text; $null when the parent holds anything else.
    #>

    param(
        $Model
        ,
        $MemberKeys
        ,
        [System.Xml.Linq.XElement]
        $Parent
    )

    $span = Get-ElementSpan -Model $Model -Element $Parent
    $text = $Model.Text
    $lineBreaks = $Model.LineBreaks
    $nodes = @($Parent.Nodes())
    # Start offset of every child node; the parent's end tag closes the last one.
    $starts = [int[]]::new($nodes.Count + 1)
    for ($i = 0; $i -lt $nodes.Count; $i++) {
        $node = $nodes[$i]
        if ($node -is [System.Xml.Linq.XText]) {
            return $null
        }
        $lineInfo = [System.Xml.IXmlLineInfo]$node
        $offset = $lineInfo.LinePosition - 1
        if ($lineInfo.LineNumber -gt 1) {
            $lineBreak = $lineBreaks[$lineInfo.LineNumber - 2]
            $offset += $lineBreak.Index + $lineBreak.Length
        }
        $starts[$i] = if ($node -is [System.Xml.Linq.XComment]) { $offset - 4 } elseif ($node -is [System.Xml.Linq.XProcessingInstruction]) { $offset - 2 } else { $offset - 1 }
    }
    $starts[$nodes.Count] = $span.ContentEnd

    $units = [System.Collections.Generic.List[object]]::new()
    $cursor = $span.StartTagEnd
    for ($i = 0; $i -lt $nodes.Count; $i++) {
        $node = $nodes[$i]
        if ($node -isnot [System.Xml.Linq.XElement]) {
            # Comments and processing instructions travel with the next member.
            continue
        }
        $key = $null
        if (-not $MemberKeys.TryGetValue($node, [ref]$key)) {
            return $null
        }
        $end = $text.LastIndexOf([char]'>', $starts[$i + 1] - 1) + 1
        $lead = $text.Substring($cursor, $starts[$i] - $cursor)
        $leadSignature = if ([string]::IsNullOrWhiteSpace($lead)) { '' } else { $lead -replace '\s+', '' }
        $units.Add([pscustomobject]@{
                Key       = $key
                Signature = "$leadSignature`0$($node.ToString($disableFormatting))"
                Text      = $text.Substring($cursor, $end - $cursor)
            })
        $cursor = $end
    }
    $trail = if (-not $span.IsEmpty) { $text.Substring($cursor, $span.ContentEnd - $cursor) } else { $null }
    return [pscustomobject]@{ Span = $span; Units = $units; Trail = $trail }
}

function ConvertTo-UnitMap {
    <#
    .SYNOPSIS
        Units by key.
    #>

    param(
        $Units
    )

    $map = [System.Collections.Generic.Dictionary[string, object]]::new()
    foreach ($unit in $Units) {
        $map[$unit.Key] = $unit
    }
    return $map
}

function Merge-CollectionUnit {
    <#
    .SYNOPSIS
        3-way merge of unordered units. An entry has a Resolved unit for all versions, or each
        version keeps its own unit (Base/Ours/Theirs) for the line-based merge. Order: ours, then
        theirs additions, then theirs-only changes (stable when applied again).
    #>

    param(
        $BaseUnits
        ,
        $OursUnits
        ,
        $TheirsUnits
    )

    $baseMap = ConvertTo-UnitMap -Units $BaseUnits
    $oursMap = ConvertTo-UnitMap -Units $OursUnits
    $theirsMap = ConvertTo-UnitMap -Units $TheirsUnits
    $plan = [System.Collections.Generic.List[object]]::new()
    $theirsAdded = [System.Collections.Generic.List[object]]::new()
    $theirsChanged = [System.Collections.Generic.List[object]]::new()

    foreach ($o in $OursUnits) {
        $b = $null
        $t = $null
        $null = $baseMap.TryGetValue($o.Key, [ref]$b)
        $null = $theirsMap.TryGetValue($o.Key, [ref]$t)
        if ($null -ne $t) {
            if ($o.Signature -ceq $t.Signature -or ($null -ne $b -and $t.Signature -ceq $b.Signature)) {
                $plan.Add(@{ Resolved = $o })
            }
            elseif ($null -ne $b -and $o.Signature -ceq $b.Signature) {
                $plan.Add(@{ Resolved = $t })
            }
            else {
                # Changed (or added) differently on both sides: left to the line-based merge.
                $plan.Add(@{ Resolved = $null; Base = $b; Ours = $o; Theirs = $t })
            }
        }
        elseif ($null -eq $b) {
            $plan.Add(@{ Resolved = $o })
        }
        elseif ($o.Signature -cne $b.Signature) {
            # Changed by ours, deleted by theirs: the line-based merge reports the conflict.
            $plan.Add(@{ Resolved = $null; Base = $b; Ours = $o })
        }
    }

    foreach ($t in $TheirsUnits) {
        if ($oursMap.ContainsKey($t.Key)) {
            continue
        }
        $b = $null
        $null = $baseMap.TryGetValue($t.Key, [ref]$b)
        if ($null -eq $b) {
            $theirsAdded.Add(@{ Resolved = $t })
        }
        elseif ($t.Signature -cne $b.Signature) {
            # Deleted by ours, changed by theirs: the line-based merge reports the conflict.
            $theirsChanged.Add(@{ Resolved = $null; Base = $b; Theirs = $t })
        }
    }

    $plan.AddRange($theirsAdded)
    $plan.AddRange($theirsChanged)
    return , $plan
}

function Select-Trail {
    <#
    .SYNOPSIS
        Text between the last member and the closing tag, merged 3-way (ours wins on conflict).
    #>

    param(
        $Base
        ,
        $Ours
        ,
        $Theirs
    )

    if ($null -ne $Ours -and $null -ne $Theirs) {
        if (
            $Ours -cne $Theirs -and
            (
                $null -eq $Base -or
                ($Ours -cne $Base -and $Theirs -cne $Base)
            ) -and
            (
                -not [string]::IsNullOrWhiteSpace($Ours) -or
                -not [string]::IsNullOrWhiteSpace($Theirs)
            )
        ) {
            throw 'Conflicting trailing comments or processing instructions.'
        }
        if ($null -ne $Base -and $Ours -ceq $Base) {
            return $Theirs
        }
        return $Ours
    }
    foreach ($candidate in $Ours, $Theirs, $Base) {
        if ($null -ne $candidate) {
            return $candidate
        }
    }
    return ''
}

function New-CollectionEdit {
    <#
    .SYNOPSIS
        Text edit that replaces the collection content of one version with the merge plan.
    #>

    param(
        $Model
        ,
        $Collection
        ,
        $Plan
        ,
        [string]
        $Side
        ,
        [string]
        $Trail
    )

    $content = [System.Text.StringBuilder]::new()
    $count = 0
    foreach ($entry in $Plan) {
        $unit = if ($null -ne $entry.Resolved) { $entry.Resolved } else { $entry.$Side }
        if ($null -ne $unit) {
            $null = $content.Append($unit.Text)
            $count++
        }
    }

    $span = $Collection.Span
    if ($count -eq 0) {
        if ($Collection.Units.Count -eq 0) {
            return $null
        }
        $startTag = $Model.Text.Substring($span.Start, $span.StartTagEnd - $span.Start)
        return [pscustomobject]@{ Start = $span.Start; End = $span.End; Text = ($startTag -replace '\s*>$', '') + ' />' }
    }

    $null = $content.Append($Trail)
    if ($span.IsEmpty) {
        $tag = $Model.Text.Substring($span.Start, $span.End - $span.Start)
        return [pscustomobject]@{
            Start = $span.Start
            End   = $span.End
            Text  = ($tag -replace '\s*/>$', '>') + $content.ToString() + "</$($span.QualifiedName)>"
        }
    }
    $newContent = $content.ToString()
    if ($Model.Text.Substring($span.StartTagEnd, $span.ContentEnd - $span.StartTagEnd) -ceq $newContent) {
        return $null
    }
    return [pscustomobject]@{ Start = $span.StartTagEnd; End = $span.ContentEnd; Text = $newContent }
}

function Test-EditOverlap {
    <#
    .SYNOPSIS
        $true when the edit overlaps an edit already planned for the same version.
    #>

    param(
        $Edits
        ,
        $Edit
    )

    foreach ($existing in $Edits) {
        if ($Edit.Start -eq $existing.Start -or ($Edit.Start -lt $existing.End -and $existing.Start -lt $Edit.End)) {
            return $true
        }
    }
    return $false
}

function Set-TextEdit {
    <#
    .SYNOPSIS
        Applies non-overlapping edits to a text.
    #>

    param(
        [string]
        $Text
        ,
        $Edits
    )

    $result = [System.Text.StringBuilder]::new()
    $cursor = 0
    foreach ($edit in ($Edits | Sort-Object -Property Start)) {
        $null = $result.Append($Text, $cursor, $edit.Start - $cursor).Append($edit.Text)
        $cursor = $edit.End
    }
    $null = $result.Append($Text, $cursor, $Text.Length - $cursor)
    return $result.ToString()
}

function Invoke-CollectionPass {
    <#
    .SYNOPSIS
        Rewrites every unordered collection found in ours or theirs to the merged member list in
        all three versions. Collections overlapping an earlier rewrite (nested ones) are deferred
        to the next pass. Returns Changed and Deferred flags.
    #>

    param(
        $Sides
        ,
        $Rules
    )

    $states = @{}
    foreach ($side in $Sides) {
        $model = ConvertTo-XmlModel -Text $side.Text
        $members = Get-CollectionMember -Model $model -Rules $Rules
        $states[$side.Name] = [pscustomobject]@{
            Side       = $side
            Model      = $model
            MemberKeys = $members.Keys
            Parents    = $members.Parents
            Edits      = [System.Collections.Generic.List[object]]::new()
        }
    }

    $candidates = [System.Collections.Generic.List[object]]::new()
    foreach ($name in 'Ours', 'Theirs') {
        foreach ($parent in $states[$name].Parents) {
            $candidates.Add([pscustomobject]@{ Name = $name; Parent = $parent })
        }
    }

    $outcome = [pscustomobject]@{ Changed = $false; Deferred = $false }
    $processed = [System.Collections.Generic.HashSet[System.Xml.Linq.XElement]]::new()
    foreach ($candidate in $candidates) {
        $steps = Get-IdentityStep -MemberKeys $states[$candidate.Name].MemberKeys -Element $candidate.Parent
        $collections = @{}
        $skip = $false
        foreach ($name in $sideNames) {
            $state = $states[$name]
            $parent = if ($name -eq $candidate.Name) {
                $candidate.Parent
            }
            else {
                Find-ByIdentityStep -Model $state.Model -MemberKeys $state.MemberKeys -Steps $steps
            }
            if ($null -eq $parent) {
                # Ours and theirs must both contain the parent; a missing base parent means "added".
                if ($name -ne 'Base') {
                    $skip = $true
                    break
                }
                $collections[$name] = $null
                continue
            }
            if ($name -eq 'Ours' -and -not $processed.Add($parent)) {
                $skip = $true
                break
            }
            $collections[$name] = Get-CollectionUnit -Model $state.Model -MemberKeys $state.MemberKeys -Parent $parent
            if ($null -eq $collections[$name]) {
                $skip = $true
                break
            }
        }
        if ($skip) {
            continue
        }

        $baseUnits = $null
        $baseTrail = $null
        if ($null -ne $collections['Base']) {
            $baseUnits = $collections['Base'].Units
            $baseTrail = $collections['Base'].Trail
        }
        $plan = Merge-CollectionUnit -BaseUnits $baseUnits -OursUnits $collections['Ours'].Units -TheirsUnits $collections['Theirs'].Units
        $trail = Select-Trail -Base $baseTrail -Ours $collections['Ours'].Trail -Theirs $collections['Theirs'].Trail

        $pending = [System.Collections.Generic.List[object]]::new()
        foreach ($name in $sideNames) {
            if ($null -eq $collections[$name]) {
                continue
            }
            $state = $states[$name]
            $edit = New-CollectionEdit -Model $state.Model -Collection $collections[$name] -Plan $plan -Side $name -Trail $trail
            if ($null -eq $edit) {
                continue
            }
            if (Test-EditOverlap -Edits $state.Edits -Edit $edit) {
                $outcome.Deferred = $true
                $skip = $true
                break
            }
            $pending.Add([pscustomobject]@{ Edits = $state.Edits; Edit = $edit })
        }
        if ($skip) {
            continue
        }
        foreach ($item in $pending) {
            $item.Edits.Add($item.Edit)
        }

        if ($pending.Count -gt 0) {
            $names = @($candidate.Parent.AncestorsAndSelf() | ForEach-Object { $_.Name.LocalName })
            [array]::Reverse($names)
            $lineBased = @($plan | Where-Object { $null -eq $_.Resolved }).Count
            Write-Verbose "/$($names -join '/'): $($plan.Count) element(s), $lineBased left to the line-based merge."
        }
    }

    foreach ($name in $sideNames) {
        $state = $states[$name]
        if ($state.Edits.Count -gt 0) {
            $state.Side.Text = Set-TextEdit -Text $state.Model.Text -Edits $state.Edits
            $outcome.Changed = $true
        }
    }
    return $outcome
}

function Invoke-StructuralMerge {
    <#
    .SYNOPSIS
        Pre-merges the unordered collections of all three versions, then merges line-based again.
        Returns the git merge-file result, or $null when nothing could be improved.
    #>

    param(
        [string]
        $WorkDirectory
    )

    try {
        $rulesFile = Resolve-RulesFile
        Write-Verbose "XML merge rules: $rulesFile"
        $rules = Read-XmlMergeRule -Path $rulesFile
        if ($rules.Count -eq 0) {
            return $null
        }

        $paths = @{ Base = $Base; Ours = $Ours; Theirs = $Theirs }
        $sides = foreach ($name in $sideNames) {
            $file = Read-XmlTextFile -Path $paths[$name]
            [pscustomobject]@{
                Name   = $name
                Text   = $file.Text
                HasBom = $file.HasBom
                Path   = (Join-Path $WorkDirectory "$name.xml")
            }
        }

        $changed = $false
        for ($pass = 1; ; $pass++) {
            $outcome = Invoke-CollectionPass -Sides $sides -Rules $rules
            $changed = $changed -or $outcome.Changed
            if (-not $outcome.Deferred) {
                break
            }
            if ($pass -ge 16) {
                throw 'Unordered collections did not converge.'
            }
        }
        if (-not $changed) {
            return $null
        }

        foreach ($side in $sides) {
            [System.IO.File]::WriteAllText($side.Path, $side.Text, [System.Text.UTF8Encoding]::new($side.HasBom))
        }
        $merged = Invoke-GitMergeFile -OursPath $sides[1].Path -BasePath $sides[0].Path -TheirsPath $sides[2].Path -ResultPath (Join-Path $WorkDirectory 'structural.xml')
        if ($merged.Conflicts -lt 0 -or $merged.Conflicts -gt 127) {
            return $null
        }
        if ($merged.Conflicts -eq 0 -and -not (Test-WellFormedXml -Bytes $merged.Bytes)) {
            Write-Warning "Order-independent XML merge of '$displayPath' produced invalid XML; keeping the line-based result."
            return $null
        }
        return $merged
    }
    catch {
        Write-Warning "Order-independent XML merge skipped for '$displayPath': $($_.Exception.Message)"
        return $null
    }
}

# ─── Entry point ─────────────────────────────────────────────────────────────

$displayPath = if ($FilePath) { $FilePath } else { $Ours }
$Base = (Resolve-Path -LiteralPath $Base).ProviderPath
$Ours = (Resolve-Path -LiteralPath $Ours).ProviderPath
$Theirs = (Resolve-Path -LiteralPath $Theirs).ProviderPath

$workDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('d365gitops-xmlmerge-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $workDirectory
$exitCode = 1
try {
    $result = Invoke-GitMergeFile -OursPath $Ours -BasePath $Base -TheirsPath $Theirs -ResultPath (Join-Path $workDirectory 'plain.xml')
    if ($result.Conflicts -lt 0 -or $result.Conflicts -gt 127) {
        Write-Warning "git merge-file failed for '$displayPath' (exit code $($result.Conflicts)); file left unmerged."
    }
    else {
        if ($result.Conflicts -gt 0) {
            $structural = Invoke-StructuralMerge -WorkDirectory $workDirectory
            if ($null -ne $structural -and $structural.Conflicts -le $result.Conflicts) {
                $result = $structural
                if ($result.Conflicts -eq 0) {
                    Write-Host "Merged order-independent XML elements: $displayPath"
                }
            }
        }
        [System.IO.File]::WriteAllBytes($Ours, $result.Bytes)
        $exitCode = [int]($result.Conflicts -gt 0)
        if ($exitCode -ne 0) {
            Write-Warning "Merge conflicts in '$displayPath' ($($result.Conflicts) conflict region(s))."
        }
    }
}
finally {
    Remove-Item -LiteralPath $workDirectory -Recurse -Force -ErrorAction Ignore
}

if ($MyInvocation.MyCommand.CommandType -eq [System.Management.Automation.CommandTypes]::ExternalScript) {
    exit $exitCode
}

return $exitCode

# SIG # Begin signature block
# MII2mQYJKoZIhvcNAQcCoII2ijCCNoYCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAD0BwMw3PvaRqg
# y1mM2FfpcWVIcRNpzmRh/0vWN7GLwKCCGzQwggXMMIIDtKADAgECAhBUmNLR1FsZ
# lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu
# dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy
# MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv
# c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo
# b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD
# Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH
# or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD
# PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV
# qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70
# ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1
# vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3
# W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK
# xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg
# trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj
# O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7
# swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE
# AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ
# T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn
# LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c
# Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF
# xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx
# AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8
# FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY
# lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2
# 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY
# uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al
# GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk
# ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T
# 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggaSMIIEeqADAgECAhMzAAiWokxQ
# x66HAhz8AAAACJaiMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ
# RCBWZXJpZmllZCBDUyBFT0MgQ0EgMDMwHhcNMjYxMDE4MDI1MDQ0WhcNMjYxMDIx
# MDI1MDQ0WjBVMQswCQYDVQQGEwJTSTESMBAGA1UEBxMJTGp1YmxqYW5hMRgwFgYD
# VQQKEw9CRS10ZXJuYSBkLm8uby4xGDAWBgNVBAMTD0JFLXRlcm5hIGQuby5vLjCC
# AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJdNhrUWIlPmS3c09jFX4He0
# J2ix0cJSDdCoG/rBpushZFn6q5hTOfiTnH970F7MQgY9LFMzSJ+H9qvmmzT1TEdV
# AehGdgC/AOlvm39uxZRnhumCOeR1HW4aG5G5v5PpxGmXJvMWN7AEZyJ58JJ8KkDF
# u6jxDXhKRZ84iYsgJWFC67i6lsVbHcE94knXw5hTypj1HZ+2Krh6gmYm3kgeqI0O
# 9UrUED5Z421D9ZWJHLNNbuUs8A17YOAML3+1Df/arSmLpeWRJ3F3hOViZRAWc0nt
# 7Xub+593HSL6YkyYRbTcIHHlRlg4xJ4/N6eoacE7d/3/VnFv9q9UGj5iA1g1hdJT
# bkOu+yaXausyD+QYSN4C7fSzsRXxisFJMXW5YCgY9F9bZMAyCNyhl4jxJe+4c8de
# /omI+uro3+JJsi/LZVLa1j8YG1pePEeoc1OjyjIuM3v+uii2DPcqqzqzb9nvBTkS
# Q9P4hVaH2qvplTxSQT2bLFhH4qXzW2br2QRX+vRERQIDAQABo4IB1DCCAdAwDAYD
# VR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwOwYDVR0lBDQwMgYKKwYBBAGCN2EB
# AAYIKwYBBQUHAwMGGisGAQQBgjdhpOaoI4H67850gfzolDeutrtWMB0GA1UdDgQW
# BBQNYRcTYM26/9pbC1jzU9rO+1c97TAfBgNVHSMEGDAWgBRrXqU0wwXFYkohWo6r
# c2Bi1KxjhTBnBgNVHR8EYDBeMFygWqBYhlZodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBJRCUyMFZlcmlmaWVkJTIwQ1MlMjBF
# T0MlMjBDQSUyMDAzLmNybDB0BggrBgEFBQcBAQRoMGYwZAYIKwYBBQUHMAKGWGh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIw
# SUQlMjBWZXJpZmllZCUyMENTJTIwRU9DJTIwQ0ElMjAwMy5jcnQwVAYDVR0gBE0w
# SzBJBgRVHSAAMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTANBgkqhkiG9w0BAQwFAAOCAgEA
# ZKezAUaS2Jm1GOnzuWmqfAKI1PXAP5E+sgfqsDxdZRoQzzQcuqJiBWCZhOe0BEFY
# OmkRXEnspD21MfIHSyRIk9VPmU9mFWqAv+jX/pQ0l8oPIowCLxo8xwiXRCXFs3bv
# Hn9FUPyr/JW0V7396uC+Y33SxXusVMf6b59n0nKAahOHtajGn2SXYDZiBbW6bLe9
# E+kGWygf9E9hMLVYSeN1ecnjAuDOmZ18GifAnVT+Um+u7OLXU5Il2zhn7Hbx2Ti4
# /coiRrPonrb5FvO6AKv9F5JQco5yyAPP63RB5rLEsos5f2phsEG4PJqQkOV8BK02
# EN63hCkp3tmKccH27H1+aWMcYdBT7q4PL7IKxp9osz5OJPpnL7/7MYbDuOKHfZKS
# ys+Gm5fqopr/UiB9m+Ul0siycMfltpgZxte1nLxFDpcKebacUnvumdLbOnmIplRQ
# eCA/po0gjh2xvcdgNbZRc/sOIE407rWgZ8t91YdvtEAZrMOumbuyUtOzS4bKs3/W
# 3NzBvgtPEjP4cKHrefYiD6d2lljA/arXWIBNyc62FW8Uqz5ltn5aqeYjFQEuPD2B
# FX2x46InMMIcoQfbYI6ItQuzkROjf+twmPN0ufKD0EWOVsL5qxLEW/s6/XZczvl0
# hDizRrkBbBlrb38oLjGhepN8ZFhS6NCangB5g5jY+nkwggcoMIIFEKADAgECAhMz
# AAAAFQU+bhmOkynZAAAAAAAVMA0GCSqGSIb3DQEBDAUAMGMxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNDAyBgNVBAMTK01pY3Jv
# c29mdCBJRCBWZXJpZmllZCBDb2RlIFNpZ25pbmcgUENBIDIwMjEwHhcNMjYwMzI2
# MTgxMTI4WhcNMzEwMzI2MTgxMTI4WjBaMQswCQYDVQQGEwJVUzEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSswKQYDVQQDEyJNaWNyb3NvZnQgSUQgVmVy
# aWZpZWQgQ1MgRU9DIENBIDAzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
# AgEA4PTLPQKqLw5zHj7zDnvism4QnfPpaJM2DkZUt5AVV7HnnG8hsAXLHp5ZuWy7
# TBj44iBS8wUBfoIZVVf1NvauRnHXhBAQh00xoS9pKCKy3OFK5YjEXG7/ZjjLUf5e
# /8QJr9BceASR59XR7d+376wal5ioynxn+Q6cjv/oZ1e0xK3jLUtfYjvm42f/R56Y
# NzwpNHu2Em0UxZMfexWcEVqQuLNzXqUX0V0If1jAI+yZrGHlWaIYuExecltiTKyW
# asB3MsyWWLQ9h5Z6OWRCZHYmXBGsRzqG5sDtOmdSfXNt6bPTxiIRmqtbCixAM/Q6
# HOay5GFhrXg67HCoQKdpCHP6GJR/SI+gZDqqoFiDRJBLQvGTRtTGpPod6OuWo9Ik
# CpncVuyGWhzuXLsqDIvirWH13iCIN7FSG0thC/JFLbAxnRKjagKv4rKk4tY16i3u
# oiqdZ4tUj3bz1vRtNwk7GBevG/8riEEcG3aAQl3pjDSQktHaKwkWOG9lgAMuJ4O0
# gDXBIKwYGX+d+fkHy1OYRs6yoyKWzGm2rlm+RSllCpDLD3FxZF0VjuJ6Cj5uClpR
# cqajqWyfyjjVUXiJcR0EXoADgcyIUQe4K/SA0NbHNjIDoEPsVRluKKuBw9JnwIsI
# si7JGa5GkOyaGp2IwTXEfUUtumMQFW3AbS4rRU8wiBIOWXUCAwEAAaOCAdwwggHY
# MA4GA1UdDwEB/wQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUa16l
# NMMFxWJKIVqOq3NgYtSsY4UwVAYDVR0gBE0wSzBJBgRVHSAAMEEwPwYIKwYBBQUH
# AgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0
# b3J5Lmh0bTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTASBgNVHRMBAf8ECDAG
# AQH/AgEAMB8GA1UdIwQYMBaAFNlBKbAPD2Ns72nX9c0pnqRIajDmMHAGA1UdHwRp
# MGcwZaBjoGGGX2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01p
# Y3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUy
# MDIwMjEuY3JsMH0GCCsGAQUFBwEBBHEwbzBtBggrBgEFBQcwAoZhaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBJRCUyMFZl
# cmlmaWVkJTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDIxLmNydDANBgkqhkiG
# 9w0BAQwFAAOCAgEAXW4iPM8Fy1/IJSRIf/ENtlDAlIVgTuOmfRT4cDkd5nZakVS5
# GDqJ/zHM1MK4w4cd1/fUjx+T0n5ZBqE75zvWVhzOWBVWKTuzWLgfpn1UhgBmcIhj
# gElpNItge75/ZxJSSZqIl8boHx+WHQbK1IE7dABTV5M5qk4JPktR8W9bv9BwqhB1
# WT5NgP+niV2G7aUTORXM9NI4rFJfQUWYEnmzg1fOWwczr3qsgt39D5xwsUSTYTG/
# MT/7Af1SO6X9q4Xkle86lEr/L5/3yDG5V3mlSJaaqKvEj/QSTIxPwqFVycZ5GUET
# NRWu5Dfcs7b0XjocUoD4KWcf15f45MMhBVSUwXwad7E4HyHP6Zqr9nobWpC9gBI+
# /BJjj0KIcSU98Ml/j+/BgNubS6QL8490TDB3fM9fGbrlYvutDAMxqTgEh9S/DZa9
# 32UWZ0Dvqcsntgwr2Jh2iH3VIGCap+56McRlb/PfkWhE4dbYAg78DaRQkhu75eQO
# GpKPtn8eNPa/U1o1wuzon9SEOWScweEX/BrwYh2I7zJh6ZXnadRRkS3UkRVaQt/z
# iqWWOmryKmae/vKT/1kD/dNw3YK7wE+luMTzgcVz2uLRpLDd0rqiWohWB0jcngbn
# 5/IrHro1uCGwUmxw+AT6mxd6mfu5xvXf3fxtvy8eJB/XApgX5rGXUpB5rpAwggee
# MIIFhqADAgECAhMzAAAAB4ejNKN7pY4cAAAAAAAHMA0GCSqGSIb3DQEBDAUAMHcx
# CzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBG
# BgNVBAMTP01pY3Jvc29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0
# aWZpY2F0ZSBBdXRob3JpdHkgMjAyMDAeFw0yMTA0MDEyMDA1MjBaFw0zNjA0MDEy
# MDE1MjBaMGMxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xNDAyBgNVBAMTK01pY3Jvc29mdCBJRCBWZXJpZmllZCBDb2RlIFNpZ25p
# bmcgUENBIDIwMjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCy8MCv
# GYgo4t1UekxJbGkIVQm0Uv96SvjB6yUo92cXdylN65Xy96q2YpWCiTas7QPTkGnK
# 9QMKDXB2ygS27EAIQZyAd+M8X+dmw6SDtzSZXyGkxP8a8Hi6EO9Zcwh5A+wOALNQ
# bNO+iLvpgOnEM7GGB/wm5dYnMEOguua1OFfTUITVMIK8faxkP/4fPdEPCXYyy8NJ
# 1fmskNhW5HduNqPZB/NkWbB9xxMqowAeWvPgHtpzyD3PLGVOmRO4ka0WcsEZqyg6
# efk3JiV/TEX39uNVGjgbODZhzspHvKFNU2K5MYfmHh4H1qObU4JKEjKGsqqA6Rzi
# ybPqhvE74fEp4n1tiY9/ootdU0vPxRp4BGjQFq28nzawuvaCqUUF2PWxh+o5/TRC
# b/cHhcYU8Mr8fTiS15kRmwFFzdVPZ3+JV3s5MulIf3II5FXeghlAH9CvicPhhP+V
# aSFW3Da/azROdEm5sv+EUwhBrzqtxoYyE2wmuHKws00x4GGIx7NTWznOm6x/niqV
# i7a/mxnnMvQq8EMse0vwX2CfqM7Le/smbRtsEeOtbnJBbtLfoAsC3TdAOnBbUkbU
# fG78VRclsE7YDDBUbgWt75lDk53yi7C3n0WkHFU4EZ83i83abd9nHWCqfnYa9qIH
# PqjOiuAgSOf4+FRcguEBXlD9mAInS7b6V0UaNwIDAQABo4ICNTCCAjEwDgYDVR0P
# AQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTZQSmwDw9jbO9p
# 1/XNKZ6kSGow5jBUBgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRt
# MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYD
# VR0jBBgwFoAUyH7SaoUqG8oZmAQHJ89QEE9oqKIwgYQGA1UdHwR9MHsweaB3oHWG
# c2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIw
# QXV0aG9yaXR5JTIwMjAyMC5jcmwwgcMGCCsGAQUFBwEBBIG2MIGzMIGBBggrBgEF
# BQcwAoZ1aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNy
# b3NvZnQlMjBJZGVudGl0eSUyMFZlcmlmaWNhdGlvbiUyMFJvb3QlMjBDZXJ0aWZp
# Y2F0ZSUyMEF1dGhvcml0eSUyMDIwMjAuY3J0MC0GCCsGAQUFBzABhiFodHRwOi8v
# b25lb2NzcC5taWNyb3NvZnQuY29tL29jc3AwDQYJKoZIhvcNAQEMBQADggIBAH8l
# Kp7+1Kvq3WYK21cjTLpebJDjW4ZbOX3HD5ZiG84vjsFXT0OB+eb+1TiJ55ns0BHl
# uC6itMI2vnwc5wDW1ywdCq3TAmx0KWy7xulAP179qX6VSBNQkRXzReFyjvF2BGt6
# FvKFR/imR4CEESMAG8hSkPYso+GjlngM8JPn/ROUrTaeU/BRu/1RFESFVgK2wMz7
# fU4VTd8NXwGZBe/mFPZG6tWwkdmA/jLbp0kNUX7elxu2+HtHo0QO5gdiKF+YTYd1
# BGrmNG8sTURvn09jAhIUJfYNotn7OlThtfQjXqe0qrimgY4Vpoq2MgDW9ESUi1o4
# pzC1zTgIGtdJ/IvY6nqa80jFOTg5qzAiRNdsUvzVkoYP7bi4wLCj+ks2GftUct+f
# GUxXMdBUv5sdr0qFPLPB0b8vq516slCfRwaktAxK1S40MCvFbbAXXpAZnU20FaAo
# Dwqq/jwzwd8Wo2J83r7O3onQbDO9TyDStgaBNlHzMMQgl95nHBYMelLEHkUnVVVT
# UsgC0Huj09duNfMaJ9ogxhPNThgq3i8w3DAGZ61AMeF0C1M+mU5eucj1Ijod5O2M
# MPeJQ3/vKBtqGZg4eTtUHt/BPjN74SsJsyHqAdXVS5c+ItyKWg3Eforhox9k3Wgt
# WTpgV4gkSiS4+A09roSdOI4vrRw+p+fL4WrxSK5nMYIauzCCGrcCAQEwcTBaMQsw
# CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSswKQYD
# VQQDEyJNaWNyb3NvZnQgSUQgVmVyaWZpZWQgQ1MgRU9DIENBIDAzAhMzAAiWokxQ
# x66HAhz8AAAACJaiMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIPLTQpPz0Y+Oz2qWTjCs
# AQPqGUgmqWKp1pXBFmRfg96EMA0GCSqGSIb3DQEBAQUABIIBgFB0eOF4e/cQUomB
# R2n93/eEMOVQt2pxjgb9DvsyjsIYtqX3kqmq9zyI1hDc3mmm5JXSwSFx3Rdi8ZKn
# f+lVUv3hGhC+Kl1wWrudpVZ4UEjYZCD24MtDyUbgPZpYPCL2syoe6GohXGk0/iTj
# Vr4XTWCzAKGrF8AmNibAigct6haZbqGJXAGy35lH8MxrrxYzHspvY6GfHFFQoBcJ
# Gg+XRUO6x23443/yfynIzioQpgqIyInvETd7QuvGDCSItYoGGdEgeJXyBi8CDnfA
# ir1jsmuMK8Se7UJLdA+lp1xQSgMtTATd/vHaqZFgAZRO17+tAQniqVGHro8ZtJ8Q
# zuIc/sdKVB3rninKtfAcjqEW88sSiT5dvUev7fgajPGkDcLKv64HfwaDgopysyzs
# IShRjQB7GT/Mw60W0o4KA39EXzrJqduS6YhHY4/nJJOHbu9NTYRp5lYvUgb2gIGU
# 58h6+9eO8sfgFVjNhcWr/5vjB2iLgYMOaURHbJoZQGcYcXEqKqGCGBQwghgQBgor
# BgEEAYI3AwMBMYIYADCCF/wGCSqGSIb3DQEHAqCCF+0wghfpAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFiBgsqhkiG9w0BCRABBKCCAVEEggFNMIIBSQIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCCveXJTUov2zdJSLwESrkdfD/PMXL6X41KB
# QC5+zi0rjAIGasjmBRDLGBMyMDI2MTAxOTEzMjgyMi40ODlaMASAAgH0oIHhpIHe
# MIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxk
# IFRTUyBFU046QTUwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJs
# aWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5oIIPITCCB4IwggVqoAMCAQIC
# EzMAAAAF5c8P/2YuyYcAAAAAAAUwDQYJKoZIhvcNAQEMBQAwdzELMAkGA1UEBhMC
# VVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjFIMEYGA1UEAxM/TWlj
# cm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBSb290IENlcnRpZmljYXRlIEF1
# dGhvcml0eSAyMDIwMB4XDTIwMTExOTIwMzIzMVoXDTM1MTExOTIwNDIzMVowYTEL
# MAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAG
# A1UEAxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjAw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCefOdSY/3gxZ8FfWO1BiKj
# HB7X55cz0RMFvWVGR3eRwV1wb3+yq0OXDEqhUhxqoNv6iYWKjkMcLhEFxvJAeNcL
# AyT+XdM5i2CgGPGcb95WJLiw7HzLiBKrxmDj1EQB/mG5eEiRBEp7dDGzxKCnTYoc
# DOcRr9KxqHydajmEkzXHOeRGwU+7qt8Md5l4bVZrXAhK+WSk5CihNQsWbzT1nRli
# VDwunuLkX1hyIWXIArCfrKM3+RHh+Sq5RZ8aYyik2r8HxT+l2hmRllBvE2Wok6IE
# aAJanHr24qoqFM9WLeBUSudz+qL51HwDYyIDPSQ3SeHtKog0ZubDk4hELQSxnfVY
# XdTGncaBnB60QrEuazvcob9n4yR65pUNBCF5qeA4QwYnilBkfnmeAjRN3LVuLr0g
# 0FXkqfYdUmj1fFFhH8k8YBozrEaXnsSL3kdTD01X+4LfIWOuFzTzuoslBrBILfHN
# j8RfOxPgjuwNvE6YzauXi4orp4Sm6tF245DaFOSYbWFK5ZgG6cUY2/bUq3g3bQAq
# Zt65KcaewEJ3ZyNEobv35Nf6xN6FrA6jF9447+NHvCjeWLCQZ3M8lgeCcnnhTFty
# QX3XgCoc6IRXvFOcPVrr3D9RPHCMS6Ckg8wggTrtIVnY8yjbvGOUsAdZbeXUIQAW
# Ms0d3cRDv09SvwVRd61evQIDAQABo4ICGzCCAhcwDgYDVR0PAQH/BAQDAgGGMBAG
# CSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRraSg6NS9IY0DPe9ivSek+2T3bITBU
# BgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNy
# b3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAUyH7SaoUqG8oZmAQHJ89QEE9oqKIwgYQGA1UdHwR9
# MHsweaB3oHWGc2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01p
# Y3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRp
# ZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcmwwgZQGCCsGAQUFBwEBBIGHMIGE
# MIGBBggrBgEFBQcwAoZ1aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# ZXJ0cy9NaWNyb3NvZnQlMjBJZGVudGl0eSUyMFZlcmlmaWNhdGlvbiUyMFJvb3Ql
# MjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMjAuY3J0MA0GCSqGSIb3DQEB
# DAUAA4ICAQBfiHbHfm21WhV150x4aPpO4dhEmSUVpbixNDmv6TvuIHv1xIs174bN
# GO/ilWMm+Jx5boAXrJxagRhHQtiFprSjMktTliL4sKZyt2i+SXncM23gRezzsoOi
# Bhv14YSd1Klnlkzvgs29XNjT+c8hIfPRe9rvVCMPiH7zPZcw5nNjthDQ+zD563I1
# nUJ6y59TbXWsuyUsqw7wXZoGzZwijWT5oc6GvD3HDokJY401uhnj3ubBhbkR83Rb
# fMvmzdp3he2bvIUztSOuFzRqrLfEvsPkVHYnvH1wtYyrt5vShiKheGpXa2AWpsod
# 4OJyT4/y0dggWi8g/tgbhmQlZqDUf3UqUQsZaLdIu/XSjgoZqDjamzCPJtOLi2hB
# wL+KsCh0Nbwc21f5xvPSwym0Ukr4o5sCcMUcSy6TEP7uMV8RX0eH/4JLEpGyae6K
# i8JYg5v4fsNGif1OXHJ2IWG+7zyjTDfkmQ1snFOTgyEX8qBpefQbF0fx6URrYiar
# jmBprwP6ZObwtZXJ23jK3Fg/9uqM3j0P01nzVygTppBabzxPAh/hHhhls6kwo3QL
# J6No803jUsZcd4JQxiYHHc+Q/wAMcPUnYKv/q2O444LO1+n6j01z5mggCSlRwD9f
# aBIySAcA9S8h22hIAcRQqIGEjolCK9F6nK9ZyX4lhthsGHumaABdWzCCB5cwggV/
# oAMCAQICEzMAAABgcgYkikoFuNkAAAAAAGAwDQYJKoZIhvcNAQEMBQAwYTELMAkG
# A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UE
# AxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjAwHhcN
# MjYwOTE3MTg1NzI3WhcNMjcwOTE0MTg1NzI3WjCB2zELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkE1MDAtMDVFMC1E
# OTQ3MTUwMwYDVQQDEyxNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lIFN0YW1waW5n
# IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJoTY0vA
# U8eK+kN8x4BKnIlH9iQTFcIiEQlfNr5cc5jWEaQDpqjZp3vU/BSRYGenvTs+oHs4
# UQfhfBePUoupwfvrE/rknRbMV05RfK585gQXn4v2eKIWStrcQldJzXDMO4SAk7xB
# KjYCuGZaXIV1a/HIWPsVCyskVGqzVxiO8mm4L4j1qZmAflvxqB5Z31ei3eWp2Dch
# +FEjk0lVUAP9SKV/vRuDu9LnZr3tJzV0zB2marXLbk+/oi2paegA9lFpINFP6M7y
# 9WVTxmN1yKqaD+ESDlNkifkPb/CAN3mXiDbQCJwSvPAvuwBb38naq7uj74DsmcJu
# +/CQJq3wQgPhYzmOH8n2ooNZSXNdf/bEfi+uwWy1648Zm5ujDYMYyfei/S48lVSk
# VuOTGxMKFn2KcZEmezeVc/dPP2uLaIMl2KoaY3Js1HcGEH17ykl2TBBecVBinn83
# 4VnHzjkRG+p31CjE1zSCChED1Fa0qdOMCn/akDtbJOiraBD9eGRNYSNgfJCwKaRv
# /wlig8yjFAk1Dcs/ExnkvzMGfLE+WreJ/BmE499S072uPWDU4oSQCwUmYnMkZ+4v
# BNzUBuS4ZVOkU5yjRglZzHSCv1BV8U5VmEEBd7Ddxc0aUl5uZNQz2o4jkKivfxgx
# BRZzgqKO6JCwoap+lR6S5LiLUEDwbj2xyOLfAgMBAAGjggHLMIIBxzAdBgNVHQ4E
# FgQUiFQodGFQBDsTCbiC3pKjt17aamIwHwYDVR0jBBgwFoAUa2koOjUvSGNAz3vY
# r0npPtk92yEwbAYDVR0fBGUwYzBhoF+gXYZbaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwUHVibGljJTIwUlNBJTIwVGltZXN0
# YW1waW5nJTIwQ0ElMjAyMDIwLmNybDB5BggrBgEFBQcBAQRtMGswaQYIKwYBBQUH
# MAKGXWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9z
# b2Z0JTIwUHVibGljJTIwUlNBJTIwVGltZXN0YW1waW5nJTIwQ0ElMjAyMDIwLmNy
# dDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB
# /wQEAwIHgDBmBgNVHSAEXzBdMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wCAYGZ4EMAQQCMA0GCSqGSIb3DQEBDAUAA4ICAQBXi+Z/KQU0lz0ZtOXY
# nG5vgrmi6XADP5Aoe8sSm6Oy2NcWIcOciGv3pTli46YVVrnxoJ5xFwaquNrhgzzg
# MxURR7gZcPIduwsZbqLEzHMbPBK7Pa4EG+zYJxxzbn3M5q+5c5+3ZCE5y8GKOJdd
# sW5JI9lcAtS9HmLW4SRI7Km4F52jh9QKzj2/68al7Z2qxLg72YgTMGzZey5EF5U5
# g0o4fAJxeVfDgprwHks+wzBt8w7cZhW381dAbHiVk89WYic017zglRodLAgtJvsV
# QcaZ/nxPJLLZCiFTvA+DGawLfa6EBWB/wCqN3Z0466vPFLgO+qqUWmYa9Ps39ohd
# ICgBwYBmDzZz2e9yB0ESTEIn0gqvzKFkGt/Dt7H2nA0dr4Mbw2fo36Oy0BV4Pggo
# B9oc+h2X4Xw/Ep03vZgGB2P0TGh4UffoydKpW7xYx2hARd4rAKlqKbEyVlQzDxMu
# O4cDiH5AMi/XOP3HCJc0tJxR0SsYTeeDl9Tf2zcGccG0hy6F+2PlUnVhH6f8K521
# KHyOSsoWoHuIGTjGV7d93McVRRga05KqTbsTawGFr/bYTH3UhELxlXpQIC8nnTI0
# JFf3vFA/5WuzefHVnc3DZVxW3zUXvxYc7H+Eq1tSJZchblyNhrGcl2NSh4QpwBpP
# KeLXeCSLgyFbaukaAV4QJdIAFjGCB0YwggdCAgEBMHgwYTELMAkGA1UEBhMCVVMx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9z
# b2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjACEzMAAABgcgYkikoF
# uNkAAAAAAGAwDQYJYIZIAWUDBAIBBQCgggSfMBEGCyqGSIb3DQEJEAIPMQIFADAa
# BgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTAx
# OTEzMjgyMlowLwYJKoZIhvcNAQkEMSIEIP8uiHhf+L0iupY/2GAbUIKCU35sXu7j
# IaKG0nC3UfArMIG5BgsqhkiG9w0BCRACLzGBqTCBpjCBozCBoAQgN8uMufC9Lni6
# 0qo8rGZxiUfs6udrTFzTX0ylelK2ArMwfDBlpGMwYTELMAkGA1UEBhMCVVMxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0
# IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjACEzMAAABgcgYkikoFuNkA
# AAAAAGAwggNhBgsqhkiG9w0BCRACEjGCA1AwggNMoYIDSDCCA0QwggIsAgEBMIIB
# CaGB4aSB3jCB2zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO
# BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEl
# MCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUGA1UECxMe
# blNoaWVsZCBUU1MgRVNOOkE1MDAtMDVFMC1EOTQ3MTUwMwYDVQQDEyxNaWNyb3Nv
# ZnQgUHVibGljIFJTQSBUaW1lIFN0YW1waW5nIEF1dGhvcml0eaIjCgEBMAcGBSsO
# AwIaAxUAl78zyMK9SWTz4i1jrQlRYqN1QjegZzBlpGMwYTELMAkGA1UEBhMCVVMx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9z
# b2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjAwDQYJKoZIhvcNAQEL
# BQACBQDugJNzMCIYDzIwMjYxMDE5MTMwMjQzWhgPMjAyNjEwMjAxMzAyNDNaMHcw
# PQYKKwYBBAGEWQoEATEvMC0wCgIFAO6Ak3MCAQAwCgIBAAICChwCAf8wBwIBAAIC
# Er0wCgIFAO6B5PMCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAK
# MAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsFAAOCAQEAFHa6euVu
# F9m8/pe+u0P9lBtxSzazBFnJ/pFQrzWRimMntEJ5+ArK924KMjHlzo8g08kbgS5l
# 8v5Gjx0447BwM8zIDFsDekdyrvzy3IbMGGKNNxkmAyyeQRxx+cQc3Af4rjKzlhjF
# 4JIeMcA3irrn8QXn0fFV9tbLWqJkUBpXE/KLDnAW4PQjZwX/hHN6sw6KJ2x7vKOX
# vJuxfrOblmCn4zulvbI1iQjtJbH8PQnABzfpPsJDyyxNOmkTFBBPSKSIoMk+O1AI
# N/A5qh3TR91FhLV2QqXVlMDvc8vKaIygcoCjuIXBCe5vBID5NBUStoCB/Upt1ZFv
# AkOhwYVgvtT/3zANBgkqhkiG9w0BAQEFAASCAgA3w15gYuTy39aZVq+3ORaEFsqY
# APB2k5zEZpIVgBp7WWqI7P1sUcrPNVtoCo4SH60CEfHCpMMcCZPsEBakowgO8GoK
# dUKuIyuKtdZuNFS8SftPVFk61xBjOYhLwFAQEzpSp+9Blt6rTl1eo3/m8RMXDowc
# q8YXgRseb7YzLKdYwM09bbLrAKEAZSAHzVj+/CZ28+S8xRUgExB6gN5z4Lr7PV6D
# duhx/u9wPPvfXNnlLl8sJ4RBne7wsdkF5PCBUxaVTMSjkqgNI2hCNdx++8m/HDR8
# 1jsi76TRIXyCx9qGYnsDkKJVx6/tiBrUUl+tNmAmcuiXuHAK+rwS/DQGT9HtbvfV
# 12w8x1OL31WvDlvqOEMNzuEW7rTnrgBEP4hA9eAP2158bHHUZVL2OTLZDcahkKGJ
# Sp9i8yMm+Ih8nMp4wwyhCa3ZGZeacIz2Q2B9PT4C8fbySFBZHpFfALZ0K/73XoZa
# pKQwE/vKdEL6pVdRpEVnuJVKh0IuW4Qj5sExpNjZyMS5+xDxW+gh7NEq+x5RYCMp
# ClqWl6Seswp3mwId7obspLxWoCAvjQAWS0Gfjxefo6MweoZxlGd6vFsCYb770ofT
# pBiuL2hW72/LmqybN2c6K+0Yjlq4/bFo9cYDPrmF8dhG+nPCJxjItRjppEyRYw3X
# H+JA6Ufa1fryi6bEsw==
# SIG # End signature block