Private/Format-OpmXmlAttributeValue.ps1
|
<#
.SYNOPSIS Escape a string for use as a PacKit XML attribute value. .DESCRIPTION Reproduces, byte-for-byte, the escaping performed by the C++ PacKit writer (platform/util/strings/StringXML.cpp StringToXMLBase with aContent = false, used by XmlWriter::WriteAttributePair). This is deliberately NOT the same as .NET's XmlWriter escaping, so the module hand-rolls it to stay byte-faithful. Rules (per UTF-16 code unit): & -> & < -> < > -> > " -> " ' -> ' code < 0x20 (incl. TAB/LF/CR) -> &#<decimal>; (e.g. TAB->	 LF-> CR-> ) code == 0x7F (DEL) ->  0x20..0xD7FF and 0xE000..0xFFFD -> literal valid surrogate pair -> literal (both units) lone/broken surrogate, 0xFFFE/0xFFFF, anything else -> &#<decimal>; #> function Format-OpmXmlAttributeValue { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [AllowNull()] [string] $Value ) if ([string]::IsNullOrEmpty($Value)) { return '' } $sb = [System.Text.StringBuilder]::new($Value.Length + 16) $len = $Value.Length for ($i = 0; $i -lt $len; $i++) { $code = [int]$Value[$i] if ($code -eq 38) { # & [void]$sb.Append('&') } elseif ($code -eq 60) { # < [void]$sb.Append('<') } elseif ($code -eq 62) { # > [void]$sb.Append('>') } elseif ($code -eq 34) { # " [void]$sb.Append('"') } elseif ($code -eq 39) { # ' [void]$sb.Append(''') } elseif ($code -lt 0x20 -or $code -eq 0x7F) { # All C0 control characters (incl. TAB/LF/CR) and DEL -> decimal char ref. [void]$sb.Append('&#').Append($code).Append(';') } elseif (($code -ge 0x20 -and $code -le 0xD7FF) -or ($code -ge 0xE000 -and $code -le 0xFFFD)) { [void]$sb.Append([char]$code) } elseif ($code -ge 0xD800 -and $code -le 0xDBFF) { # High surrogate: keep a valid pair literal, else encode the broken unit. if (($i + 1) -lt $len) { $next = [int]$Value[$i + 1] if ($next -ge 0xDC00 -and $next -le 0xDFFF) { [void]$sb.Append([char]$code).Append([char]$next) $i++ continue } } [void]$sb.Append('&#').Append($code).Append(';') } else { # Lone low surrogate, 0xFFFE, 0xFFFF, etc. [void]$sb.Append('&#').Append($code).Append(';') } } return $sb.ToString() } <# .SYNOPSIS Test whether a string contains only characters that are valid in XML 1.0. .DESCRIPTION The byte-faithful escaper mirrors PacKit's C++ writer, which emits decimal character references (e.g. ) for characters that are not valid in XML 1.0. Such references are NOT well-formed XML and a standard parser (including PacKit's Expat reader) would reject the fragment before it could be loaded. XML 1.0 permits TAB (0x09), LF (0x0A), CR (0x0D), 0x20-0xD7FF, 0xE000-0xFFFD, and supplementary characters 0x10000-0x10FFFF (encoded in UTF-16 as a valid high+low surrogate PAIR). Everything else is invalid: the other C0 controls, 0xFFFE, 0xFFFF, and lone/broken surrogate code units. Returns $true when the text is safe to serialize, $false when it contains an XML-invalid character. #> function Test-OpmXmlSafeText { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory, Position = 0)] [AllowEmptyString()] [AllowNull()] [string] $Value ) if ([string]::IsNullOrEmpty($Value)) { return $true } for ($i = 0; $i -lt $Value.Length; $i++) { $code = [int]$Value[$i] if ($code -eq 0x09 -or $code -eq 0x0A -or $code -eq 0x0D) { continue # TAB / LF / CR } if ($code -ge 0x20 -and $code -le 0xD7FF) { continue # BMP before the surrogate range } if ($code -ge 0xE000 -and $code -le 0xFFFD) { continue # BMP after the surrogate range (excludes 0xFFFE / 0xFFFF) } if ($code -ge 0xD800 -and $code -le 0xDBFF) { # High surrogate: valid only when followed by a low surrogate. if (($i + 1) -lt $Value.Length) { $next = [int]$Value[$i + 1] if ($next -ge 0xDC00 -and $next -le 0xDFFF) { $i++ # consume the valid pair continue } } return $false # lone / broken high surrogate } # Other C0 controls, 0xFFFE, 0xFFFF, or a lone low surrogate. return $false } return $true } |