FilterBox/SRxCustomizationJson.psm1
|
<# ================================================================================ SRxCustomizationJson.psm1 ----------------------------------------------------------------------------- Shared, PURE helpers for the JSON customization storage format. No PnP/CSOM, no WinForms - safe to import in the worker, the reader context, and Explorer. A JSON customization = ONE term under a layer's "Customizations" host, carrying the customization subtree (in the provisioning-DB shape the reader already consumes) as a gzip+base64 payload, chunked at 9000 chars. Mandatory term custom properties: ts_ProvisioningActivity = 'Optimize-SiteTemplate' ts_CustomizationFormat = 'JSON' ts_CustomizationKey = '<leaf xPath>|<optype>|<scope>' ts_CustomizationJson = <payload> (writer may store as-is; CSOM chunks to _0.._n) Subtree shape (matches Convert-SRxFromXML / the reader's *_JSON helpers): { "Name":"..", "IsDeprecated":false, "CustomProperties":[ {"Key":"..","Value":".."}, .. ], "Terms":[ { ..same.. } ] } ================================================================================ #> $script:SRxJsonChunkSize = 9000 # ---------- gzip + base64 (compress/expand) ---------------------------------- function Compress-SRxText { [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) if ([string]::IsNullOrEmpty($Text)) { return '' } $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) $ms = New-Object System.IO.MemoryStream $gz = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Compress) $gz.Write($bytes, 0, $bytes.Length) $gz.Close() $out = [Convert]::ToBase64String($ms.ToArray()) $ms.Dispose() return $out } function Expand-SRxText { [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$Base64) if ([string]::IsNullOrEmpty($Base64)) { return '' } $data = [Convert]::FromBase64String($Base64) $ms = New-Object System.IO.MemoryStream(,$data) $gz = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Decompress) $sr = New-Object System.IO.StreamReader($gz, [System.Text.Encoding]::UTF8) $out = $sr.ReadToEnd() $sr.Close(); $gz.Dispose(); $ms.Dispose() return $out } # ---------- chunk / join a long property value ------------------------------- # Split -> @{ 'ts_CustomizationJson_0'=..; ... } (only if > chunk size; else single base key) function Split-SRxJsonProperty { [CmdletBinding()] param([Parameter(Mandatory)][string]$BaseKey, [Parameter(Mandatory)][AllowEmptyString()][string]$Value, [int]$ChunkSize = $script:SRxJsonChunkSize) $out = @{} if ($Value.Length -le $ChunkSize) { $out[$BaseKey] = $Value; return $out } $parts = @($Value -split ("(.{$ChunkSize})") | Where-Object { $_ }) for ([int]$i = 0; $i -lt $parts.Count; $i++) { $out["$($BaseKey)_$($i)"] = $parts[$i] } return $out } # Reassemble ts_CustomizationJson (single or _0.._n) from a properties-array/hashtable. function Join-SRxJsonProperty { [CmdletBinding()] param([Parameter(Mandatory)]$Properties, [string]$BaseKey = 'ts_CustomizationJson') # normalize to @{key=value} $map = @{} if ($Properties -is [hashtable]) { foreach ($k in $Properties.Keys) { $map[[string]$k] = [string]$Properties[$k] } } else { foreach ($p in @($Properties)) { if ($null -eq $p) { continue } $k = if ($p.PSObject.Properties['Key']) { [string]$p.Key } else { $null } if ($null -ne $k) { $map[$k] = [string]$p.Value } } } if ($map.ContainsKey($BaseKey)) { return [string]$map[$BaseKey] } $chunkKeys = @($map.Keys | Where-Object { $_ -match ("^" + [regex]::Escape($BaseKey) + "_\d+$") } | Sort-Object { [int]($_ -replace ("^" + [regex]::Escape($BaseKey) + "_"), '') }) if ($chunkKeys.Count -eq 0) { return $null } $sb = New-Object System.Text.StringBuilder foreach ($ck in $chunkKeys) { [void]$sb.Append($map[$ck]) } return $sb.ToString() } # ---------- identity key ------------------------------------------------------ function Get-SRxCustomizationKey { [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyString()][string]$LeafXPath, [int]$Optype = -1, [int]$Scope = -1) return ("{0}|{1}|{2}" -f $LeafXPath, $Optype, $Scope) } function Get-SRxShortHash { [CmdletBinding()] param([Parameter(Mandatory)][string]$Text) $md5 = [System.Security.Cryptography.MD5]::Create() $bytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Text)) $md5.Dispose() return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '').Substring(0, 8) } # ---------- attribute helpers ------------------------------------------------- function ConvertTo-SRxAttrHash { param($Attributes) $h = @{} if ($Attributes -is [hashtable]) { foreach ($k in $Attributes.Keys) { $h[[string]$k] = [string]$Attributes[$k] } } elseif ($null -ne $Attributes) { foreach ($p in $Attributes.PSObject.Properties) { $h[$p.Name] = [string]$p.Value } } return $h } # ---------- BranchPack (leaf-first) -> nested subtree (topmost root) ---------- # Each node: { Name; IsDeprecated; CustomProperties:[{Key,Value}]; Terms:[..] }. function ConvertTo-SRxCustomizationSubtree { [CmdletBinding()] param([Parameter(Mandatory)]$Branch) $arr = @($Branch) if ($arr.Count -eq 0) { return $null } $node = $null for ([int]$i = 0; $i -lt $arr.Count; $i++) { # i=0 leaf -> build upward $d = $arr[$i] $attrs = ConvertTo-SRxAttrHash $d.Attributes $isDep = $false if ($attrs.ContainsKey('ts_Deprecated')) { $isDep = ([string]$attrs['ts_Deprecated'] -ieq 'true') } $cp = @() foreach ($k in ($attrs.Keys | Sort-Object)) { $cp += [pscustomobject]@{ Key = [string]$k; Value = [string]$attrs[$k] } } $newNode = [pscustomobject]@{ Name = [string]$d.Name IsDeprecated = [bool]$isDep CustomProperties = $cp Terms = @() } if ($null -ne $node) { $newNode.Terms = @($node) } $node = $newNode } return $node # topmost ancestor } # ---------- normalize a deserialized subtree (from ConvertFrom-Json) ---------- # Ensures arrays + IsDeprecated on every node, so the reader's *_JSON helpers # consume it identically to a live term subtree. function Repair-SRxSubtree { [CmdletBinding()] param([Parameter(Mandatory)]$Node) if ($null -eq $Node) { return $null } $cp = @() if ($Node.PSObject.Properties['CustomProperties'] -and $null -ne $Node.CustomProperties) { foreach ($p in @($Node.CustomProperties)) { if ($null -eq $p) { continue } $cp += [pscustomobject]@{ Key = [string]$p.Key; Value = [string]$p.Value } } } $terms = @() if ($Node.PSObject.Properties['Terms'] -and $null -ne $Node.Terms) { foreach ($t in @($Node.Terms)) { $rt = Repair-SRxSubtree -Node $t; if ($rt) { $terms += $rt } } } $isDep = $false if ($Node.PSObject.Properties['IsDeprecated']) { $isDep = [bool]$Node.IsDeprecated } return [pscustomobject]@{ Name = [string]$Node.Name IsDeprecated = [bool]$isDep CustomProperties = $cp Terms = $terms } } # ---------- reader-facing: term (with CustomProperties) -> normalized root ---- # $Term is a provisioning-DB term object whose CustomProperties hold the JSON # payload (single or chunked). Returns the reassembled, normalized subtree root. function Expand-SRxJsonCustomization { [CmdletBinding()] param([Parameter(Mandatory)]$Term) try { $props = if ($Term.PSObject.Properties['CustomProperties']) { $Term.CustomProperties } else { @() } $payload = Join-SRxJsonProperty -Properties $props -BaseKey 'ts_CustomizationJson' if ([string]::IsNullOrEmpty($payload)) { return $null } $json = Expand-SRxText -Base64 $payload if ([string]::IsNullOrEmpty($json)) { return $null } $obj = $json | ConvertFrom-Json return (Repair-SRxSubtree -Node $obj) } catch { Write-Host "Expand-SRxJsonCustomization - $($_.Exception.Message)"; return $null } } # ---------- writer-facing: BranchPack -> single container term descriptor ----- # Produces the single-node "nodesStack" term for CustomizationWriter.Start: # Name = stable 'C_<hash(key)>' (idempotent upsert by name) # Attributes = { ts_ProvisioningActivity; ts_CustomizationFormat=JSON; # ts_CustomizationKey; ts_CustomizationJson=<compressed payload> } # CustomizationWriter chunks the long ts_CustomizationJson to _0.._n automatically. function New-SRxJsonContainerTerm { [CmdletBinding()] param([Parameter(Mandatory)]$Pack, [Parameter(Mandatory)][string]$Operation) $branch = $Pack.Branch $leafX = '' if (@($branch).Count -gt 0) { $leafX = [string](@($branch)[0].XPath) } $optype = -1 if ($Pack.PSObject.Properties['Optype'] -and $null -ne $Pack.Optype) { $optype = [int]$Pack.Optype } $scope = -1 if ($Pack.PSObject.Properties['Scope'] -and $null -ne $Pack.Scope) { $scope = [int]$Pack.Scope } $key = Get-SRxCustomizationKey -LeafXPath $leafX -Optype $optype -Scope $scope $name = 'C_' + (Get-SRxShortHash $key) $attrs = @{ 'ts_ProvisioningActivity' = 'Optimize-SiteTemplate' 'ts_CustomizationFormat' = 'JSON' 'ts_CustomizationKey' = $key } if ($Operation -ne 'remove') { $subtree = ConvertTo-SRxCustomizationSubtree -Branch $branch $json = $subtree | ConvertTo-Json -Depth 60 -Compress $attrs['ts_CustomizationJson'] = (Compress-SRxText -Text $json) } return [pscustomobject]@{ Name = $name Attributes = $attrs Scope = $scope xPath = $leafX Operation = $Operation optype = $optype Key = $key } } Export-ModuleMember -Function ` Compress-SRxText, Expand-SRxText, ` Split-SRxJsonProperty, Join-SRxJsonProperty, ` Get-SRxCustomizationKey, Get-SRxShortHash, ` ConvertTo-SRxAttrHash, ConvertTo-SRxCustomizationSubtree, Repair-SRxSubtree, ` Expand-SRxJsonCustomization, New-SRxJsonContainerTerm |