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 |