From 2f566d0ff60f525008bc449a9f6c2cd12d3270cc Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:03:22 +0200 Subject: [PATCH 1/8] build: bundle pinned YamlDotNet dependency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/THIRD-PARTY-NOTICES.txt | 18 ++++++++++++++++++ src/assemblies/YamlDotNet.dll | Bin 0 -> 298496 bytes src/licenses/YamlDotNet.LICENSE.txt | 19 +++++++++++++++++++ src/manifest.psd1 | 3 +++ 4 files changed, 40 insertions(+) create mode 100644 src/THIRD-PARTY-NOTICES.txt create mode 100644 src/assemblies/YamlDotNet.dll create mode 100644 src/licenses/YamlDotNet.LICENSE.txt create mode 100644 src/manifest.psd1 diff --git a/src/THIRD-PARTY-NOTICES.txt b/src/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..3cda06f --- /dev/null +++ b/src/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,18 @@ +YamlDotNet 18.1.0 +================= + +Copyright (c) Antoine Aubry and contributors. +Licensed under the MIT License. See licenses/YamlDotNet.LICENSE.txt. + +Package: + https://api.nuget.org/v3-flatcontainer/yamldotnet/18.1.0/yamldotnet.18.1.0.nupkg +Package repository: + https://github.com/aaubry/YamlDotNet/tree/748334a8fa7c227740018b284b71ad95cc6b7fc7 +Bundled asset: + lib/netstandard2.0/YamlDotNet.dll +Target framework: + netstandard2.0 +Package SHA-256: + 59FFE65ADE67AD9D886267F877B634A450363CB81B94E19DB9CA4C36461416F6 +Bundled DLL SHA-256: + 91CD6D1FD0AE5B64BF6B252EB3B1AF2382CBC599C29045427C45FE8403D4295D diff --git a/src/assemblies/YamlDotNet.dll b/src/assemblies/YamlDotNet.dll new file mode 100644 index 0000000000000000000000000000000000000000..55aec985b577e44ee75054c1d023425b64f33361 GIT binary patch literal 298496 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 Uf!VmlH_cA<{0p4>zfRfu|LoaI(f|Me literal 0 HcmV?d00001 diff --git a/src/licenses/YamlDotNet.LICENSE.txt b/src/licenses/YamlDotNet.LICENSE.txt new file mode 100644 index 0000000..d4f2924 --- /dev/null +++ b/src/licenses/YamlDotNet.LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/manifest.psd1 b/src/manifest.psd1 new file mode 100644 index 0000000..0f92e69 --- /dev/null +++ b/src/manifest.psd1 @@ -0,0 +1,3 @@ +@{ + DotNetFrameworkVersion = '4.7.2' +} From 21959d7b09cc67cdc1b0f60f5084177211c23b27 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:03:36 +0200 Subject: [PATCH 2/8] ci: adopt Process-PSModule 6.1.4 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Process-PSModule.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 67f2c25..f524633 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,5 +27,10 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@11117919e65242d3388727819a751f74ad24ea9e # v5.5.0 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@da180bac16b13bfbcdf08b2e4e221b5b49e5ff28 # v6.1.4 + with: + ImportantFilePatterns: | + ^src/ + ^tests/ + ^README\.md$ secrets: inherit From 488b4e3509cd9f09f50ed6862cee58d202402ef5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:54:02 +0200 Subject: [PATCH 3/8] feat: implement safe YAML parsing Add YAML 1.2 core-schema construction, safe tag handling, duplicate-key validation, aliases, resource limits, object projection, Test-Yaml, and pinned conformance fixtures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/ConvertFrom-YamlInteger.ps1 | 52 ++++ .../private/ConvertFrom-YamlNode.ps1 | 100 +++++++ .../ConvertTo-YamlParserCompatibleText.ps1 | 112 ++++++++ .../private/Get-YamlFingerprintHash.ps1 | 19 ++ .../private/Get-YamlNodeFingerprint.ps1 | 78 ++++++ .../private/Get-YamlScalarFingerprint.ps1 | 46 ++++ src/functions/private/New-YamlErrorRecord.ps1 | 38 +++ src/functions/private/New-YamlException.ps1 | 34 +++ src/functions/private/New-YamlNode.ps1 | 44 +++ src/functions/private/Read-YamlNode.ps1 | 144 ++++++++++ src/functions/private/Read-YamlStream.ps1 | 64 +++++ src/functions/private/Read-YamlStreamCore.ps1 | 90 ++++++ src/functions/private/Resolve-YamlScalar.ps1 | 159 +++++++++++ src/functions/private/Test-YamlNodeGraph.ps1 | 142 ++++++++++ src/functions/public/ConvertFrom-Yaml.ps1 | 117 ++++++++ src/functions/public/Get-PSModuleTest.ps1 | 23 -- src/functions/public/Test-Yaml.ps1 | 78 ++++++ tests/ConvertFrom-Yaml.Tests.ps1 | 259 ++++++++++++++++++ tests/PSModuleTest.Tests.ps1 | 16 -- tests/Specification.Tests.ps1 | 206 ++++++++++++++ tests/Test-Yaml.Tests.ps1 | 86 ++++++ tests/TestBootstrap.ps1 | 10 + tests/fixtures/yaml-spec-1.2.2/SOURCES.txt | 18 ++ .../yaml-spec-1.2.2/chapter-02/2.01.yaml | 3 + .../yaml-spec-1.2.2/chapter-02/2.02.yaml | 3 + .../yaml-spec-1.2.2/chapter-02/2.03.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.04.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.05.yaml | 3 + .../yaml-spec-1.2.2/chapter-02/2.06.yaml | 5 + .../yaml-spec-1.2.2/chapter-02/2.07.yaml | 10 + .../yaml-spec-1.2.2/chapter-02/2.08.yaml | 10 + .../yaml-spec-1.2.2/chapter-02/2.09.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.10.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.11.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.12.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.13.yaml | 4 + .../yaml-spec-1.2.2/chapter-02/2.14.yaml | 4 + .../yaml-spec-1.2.2/chapter-02/2.15.yaml | 8 + .../yaml-spec-1.2.2/chapter-02/2.16.yaml | 7 + .../yaml-spec-1.2.2/chapter-02/2.17.yaml | 7 + .../yaml-spec-1.2.2/chapter-02/2.18.yaml | 6 + .../yaml-spec-1.2.2/chapter-02/2.19.yaml | 4 + .../yaml-spec-1.2.2/chapter-02/2.20.yaml | 5 + .../yaml-spec-1.2.2/chapter-02/2.21.yaml | 3 + .../yaml-spec-1.2.2/chapter-02/2.22.yaml | 4 + .../yaml-spec-1.2.2/chapter-02/2.23.yaml | 13 + .../yaml-spec-1.2.2/chapter-02/2.24.yaml | 14 + .../yaml-spec-1.2.2/chapter-02/2.25.yaml | 7 + .../yaml-spec-1.2.2/chapter-02/2.26.yaml | 7 + .../yaml-spec-1.2.2/chapter-02/2.27.yaml | 29 ++ .../yaml-spec-1.2.2/chapter-02/2.28.yaml | 26 ++ tests/fixtures/yaml-test-suite/2JQS.yaml | 2 + tests/fixtures/yaml-test-suite/565N.yaml | 12 + tests/fixtures/yaml-test-suite/6BFJ.yaml | 3 + tests/fixtures/yaml-test-suite/EHF6.yaml | 4 + tests/fixtures/yaml-test-suite/H7TQ.yaml | 2 + tests/fixtures/yaml-test-suite/LICENSE.txt | 21 ++ tests/fixtures/yaml-test-suite/M5DY.yaml | 9 + tests/fixtures/yaml-test-suite/SBG9.yaml | 1 + tests/fixtures/yaml-test-suite/SF5V.yaml | 3 + tests/fixtures/yaml-test-suite/SOURCES.txt | 22 ++ .../yaml-test-suite/VJP3-invalid.yaml | 5 + .../fixtures/yaml-test-suite/VJP3-valid.yaml | 5 + 63 files changed, 2215 insertions(+), 39 deletions(-) create mode 100644 src/functions/private/ConvertFrom-YamlInteger.ps1 create mode 100644 src/functions/private/ConvertFrom-YamlNode.ps1 create mode 100644 src/functions/private/ConvertTo-YamlParserCompatibleText.ps1 create mode 100644 src/functions/private/Get-YamlFingerprintHash.ps1 create mode 100644 src/functions/private/Get-YamlNodeFingerprint.ps1 create mode 100644 src/functions/private/Get-YamlScalarFingerprint.ps1 create mode 100644 src/functions/private/New-YamlErrorRecord.ps1 create mode 100644 src/functions/private/New-YamlException.ps1 create mode 100644 src/functions/private/New-YamlNode.ps1 create mode 100644 src/functions/private/Read-YamlNode.ps1 create mode 100644 src/functions/private/Read-YamlStream.ps1 create mode 100644 src/functions/private/Read-YamlStreamCore.ps1 create mode 100644 src/functions/private/Resolve-YamlScalar.ps1 create mode 100644 src/functions/private/Test-YamlNodeGraph.ps1 create mode 100644 src/functions/public/ConvertFrom-Yaml.ps1 delete mode 100644 src/functions/public/Get-PSModuleTest.ps1 create mode 100644 src/functions/public/Test-Yaml.ps1 create mode 100644 tests/ConvertFrom-Yaml.Tests.ps1 delete mode 100644 tests/PSModuleTest.Tests.ps1 create mode 100644 tests/Specification.Tests.ps1 create mode 100644 tests/Test-Yaml.Tests.ps1 create mode 100644 tests/TestBootstrap.ps1 create mode 100644 tests/fixtures/yaml-spec-1.2.2/SOURCES.txt create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.01.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.02.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.03.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.04.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.05.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.06.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.07.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.08.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.09.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.10.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.11.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.12.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.13.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.14.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.15.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.16.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.17.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.18.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.19.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.20.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.21.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.22.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.23.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.24.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.25.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.26.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.27.yaml create mode 100644 tests/fixtures/yaml-spec-1.2.2/chapter-02/2.28.yaml create mode 100644 tests/fixtures/yaml-test-suite/2JQS.yaml create mode 100644 tests/fixtures/yaml-test-suite/565N.yaml create mode 100644 tests/fixtures/yaml-test-suite/6BFJ.yaml create mode 100644 tests/fixtures/yaml-test-suite/EHF6.yaml create mode 100644 tests/fixtures/yaml-test-suite/H7TQ.yaml create mode 100644 tests/fixtures/yaml-test-suite/LICENSE.txt create mode 100644 tests/fixtures/yaml-test-suite/M5DY.yaml create mode 100644 tests/fixtures/yaml-test-suite/SBG9.yaml create mode 100644 tests/fixtures/yaml-test-suite/SF5V.yaml create mode 100644 tests/fixtures/yaml-test-suite/SOURCES.txt create mode 100644 tests/fixtures/yaml-test-suite/VJP3-invalid.yaml create mode 100644 tests/fixtures/yaml-test-suite/VJP3-valid.yaml diff --git a/src/functions/private/ConvertFrom-YamlInteger.ps1 b/src/functions/private/ConvertFrom-YamlInteger.ps1 new file mode 100644 index 0000000..e40cf64 --- /dev/null +++ b/src/functions/private/ConvertFrom-YamlInteger.ps1 @@ -0,0 +1,52 @@ +function ConvertFrom-YamlInteger { + <# + .SYNOPSIS + Constructs the narrowest supported numeric value from a YAML integer. + #> + [CmdletBinding()] + [OutputType([int], [long], [System.Numerics.BigInteger])] + param ( + [Parameter(Mandatory)] + [string] $Value + ) + + $sign = [System.Numerics.BigInteger]::One + $digits = $Value + $base = 10 + + if ($digits.StartsWith('+', [System.StringComparison]::Ordinal)) { + $digits = $digits.Substring(1) + } elseif ($digits.StartsWith('-', [System.StringComparison]::Ordinal)) { + $sign = [System.Numerics.BigInteger]::MinusOne + $digits = $digits.Substring(1) + } + + if ($digits.StartsWith('0o', [System.StringComparison]::Ordinal)) { + $base = 8 + $digits = $digits.Substring(2) + } elseif ($digits.StartsWith('0x', [System.StringComparison]::Ordinal)) { + $base = 16 + $digits = $digits.Substring(2) + } + + $result = [System.Numerics.BigInteger]::Zero + foreach ($character in $digits.ToCharArray()) { + if ($character -ge [char] '0' -and $character -le [char] '9') { + $digit = [int] $character - [int] [char] '0' + } else { + $digit = 10 + ([int] [char]::ToUpperInvariant($character) - [int] [char] 'A') + } + $result = ($result * $base) + $digit + } + $result *= $sign + + if ($result -ge [int]::MinValue -and $result -le [int]::MaxValue) { + Write-Output -InputObject ([int] $result) -NoEnumerate + return + } + if ($result -ge [long]::MinValue -and $result -le [long]::MaxValue) { + Write-Output -InputObject ([long] $result) -NoEnumerate + return + } + Write-Output -InputObject $result -NoEnumerate +} diff --git a/src/functions/private/ConvertFrom-YamlNode.ps1 b/src/functions/private/ConvertFrom-YamlNode.ps1 new file mode 100644 index 0000000..3b1b120 --- /dev/null +++ b/src/functions/private/ConvertFrom-YamlNode.ps1 @@ -0,0 +1,100 @@ +function ConvertFrom-YamlNode { + <# + .SYNOPSIS + Projects an internal YAML node graph to PowerShell values. + #> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.Dictionary[int, object]] $Cache, + + [switch] $AsHashtable + ) + + if ($Node.Kind -eq 'Alias') { + Write-Output -InputObject (ConvertFrom-YamlNode -Node $Node.Target -Cache $Cache -AsHashtable:$AsHashtable) -NoEnumerate + return + } + + if ($Node.Kind -eq 'Scalar') { + Write-Output -InputObject (Resolve-YamlScalar -Node $Node) -NoEnumerate + return + } + + if ($Cache.ContainsKey($Node.Id)) { + Write-Output -InputObject ($Cache[$Node.Id]) -NoEnumerate + return + } + + if ($Node.Kind -eq 'Sequence') { + if ($Node.Tag -eq 'tag:yaml.org,2002:omap') { + $orderedMap = [System.Collections.Specialized.OrderedDictionary]::new() + $Cache[$Node.Id] = $orderedMap + foreach ($item in $Node.Items) { + $entryMap = ConvertFrom-YamlNode -Node $item -Cache $Cache -AsHashtable + $key = @($entryMap.Keys)[0] + $orderedMap.Add($key, $entryMap[$key]) + } + Write-Output -InputObject $orderedMap -NoEnumerate + return + } + + $isPairs = $Node.Tag -eq 'tag:yaml.org,2002:pairs' + $sequence = [object[]]::new($Node.Items.Count) + $Cache[$Node.Id] = $sequence + for ($index = 0; $index -lt $Node.Items.Count; $index++) { + $sequence[$index] = ConvertFrom-YamlNode -Node $Node.Items[$index] -Cache $Cache ` + -AsHashtable:($AsHashtable -or $isPairs) + } + Write-Output -InputObject $sequence -NoEnumerate + return + } + + $isSet = $Node.Tag -eq 'tag:yaml.org,2002:set' + if ($AsHashtable -or $isSet) { + $dictionary = [System.Collections.Specialized.OrderedDictionary]::new() + $Cache[$Node.Id] = $dictionary + foreach ($entry in $Node.Entries) { + $key = ConvertFrom-YamlNode -Node $entry.Key -Cache $Cache -AsHashtable:$AsHashtable + if ($null -eq $key) { + $key = [System.DBNull]::Value + } + $entryValue = if ($isSet) { + $null + } else { + ConvertFrom-YamlNode -Node $entry.Value -Cache $Cache -AsHashtable:$AsHashtable + } + $dictionary.Add($key, $entryValue) + } + Write-Output -InputObject $dictionary -NoEnumerate + return + } + + $result = [pscustomobject]@{} + $Cache[$Node.Id] = $result + $propertyNames = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase + ) + foreach ($entry in $Node.Entries) { + $key = ConvertFrom-YamlNode -Node $entry.Key -Cache $Cache + if ($key -isnot [string] -or [string]::IsNullOrEmpty($key)) { + throw (New-YamlException -Start $entry.Key.Start -End $entry.Key.End -ErrorId 'YamlMappingKeyNotString' -Message ( + 'This mapping key cannot be represented as a PSCustomObject property. Use -AsHashtable.' + )) + } + if (-not $propertyNames.Add($key)) { + throw (New-YamlException -Start $entry.Key.Start -End $entry.Key.End -ErrorId 'YamlPropertyNameCollision' -Message ( + "The mapping keys contain a case-insensitive property collision for '$key'. Use -AsHashtable." + )) + } + $entryValue = ConvertFrom-YamlNode -Node $entry.Value -Cache $Cache + $property = [System.Management.Automation.PSNoteProperty]::new($key, $entryValue) + $result.PSObject.Properties.Add($property) + } + Write-Output -InputObject $result -NoEnumerate +} diff --git a/src/functions/private/ConvertTo-YamlParserCompatibleText.ps1 b/src/functions/private/ConvertTo-YamlParserCompatibleText.ps1 new file mode 100644 index 0000000..5124877 --- /dev/null +++ b/src/functions/private/ConvertTo-YamlParserCompatibleText.ps1 @@ -0,0 +1,112 @@ +function ConvertTo-YamlParserCompatibleText { + <# + .SYNOPSIS + Folds valid multiline flow syntax rejected by the YamlDotNet parser. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Yaml + ) + + $lines = [System.Text.RegularExpressions.Regex]::Split($Yaml, '\r?\n') + if ($lines.Count -le 1) { + return $Yaml + } + + $builder = [System.Text.StringBuilder]::new() + $flowIndents = [System.Collections.Generic.Stack[int]]::new() + $trimLeading = $false + $inSingleQuote = $false + $inDoubleQuote = $false + + for ($lineIndex = 0; $lineIndex -lt $lines.Count; $lineIndex++) { + $originalLine = $lines[$lineIndex] + $leadingLength = $originalLine.Length - $originalLine.TrimStart(' ').Length + $line = if ($trimLeading) { + $originalLine.TrimStart(' ') + } else { + $originalLine + } + $trimLeading = $false + $hasComment = $false + $previousSignificant = [char] 0 + + for ($characterIndex = 0; $characterIndex -lt $line.Length; $characterIndex++) { + $character = $line[$characterIndex] + + if ($inDoubleQuote) { + if ($character -eq '\') { + $characterIndex++ + } elseif ($character -eq '"') { + $inDoubleQuote = $false + } + continue + } + + if ($inSingleQuote) { + if ($character -eq "'") { + if ($characterIndex + 1 -lt $line.Length -and $line[$characterIndex + 1] -eq "'") { + $characterIndex++ + } else { + $inSingleQuote = $false + } + } + continue + } + + $commentStart = $characterIndex -eq 0 -or [char]::IsWhiteSpace($line[$characterIndex - 1]) + if ($character -eq '#' -and $commentStart) { + $hasComment = $true + break + } + if ($character -eq '"') { + $inDoubleQuote = $true + continue + } + if ($character -eq "'") { + $inSingleQuote = $true + continue + } + + if ($character -eq '{' -or $character -eq '[') { + $isCollectionStart = $previousSignificant -eq [char] 0 + $isCollectionStart = $isCollectionStart -or $previousSignificant -in @(':', '-', '?', ',', '[', '{') + if ($isCollectionStart) { + $flowIndents.Push($leadingLength) + } + } elseif (($character -eq '}' -or $character -eq ']') -and $flowIndents.Count -gt 0) { + [void] $flowIndents.Pop() + } + + if (-not [char]::IsWhiteSpace($character)) { + $previousSignificant = $character + } + } + + [void] $builder.Append($line) + if ($lineIndex -eq $lines.Count - 1) { + continue + } + + $nextLine = $lines[$lineIndex + 1] + $nextIndent = $nextLine.Length - $nextLine.TrimStart(' ').Length + $canFoldStructuralLine = $flowIndents.Count -gt 0 + $canFoldStructuralLine = $canFoldStructuralLine -and -not $hasComment + $canFoldStructuralLine = $canFoldStructuralLine -and -not $inSingleQuote + $canFoldStructuralLine = $canFoldStructuralLine -and -not $inDoubleQuote + $canFoldStructuralLine = $canFoldStructuralLine -and $nextLine.Trim().Length -gt 0 + $canFoldStructuralLine = $canFoldStructuralLine -and $nextIndent -gt $flowIndents.Peek() + + if ($canFoldStructuralLine) { + [void] $builder.Append(' ') + $trimLeading = $true + } else { + [void] $builder.Append("`n") + } + } + + return $builder.ToString() +} diff --git a/src/functions/private/Get-YamlFingerprintHash.ps1 b/src/functions/private/Get-YamlFingerprintHash.ps1 new file mode 100644 index 0000000..fbdbf30 --- /dev/null +++ b/src/functions/private/Get-YamlFingerprintHash.ps1 @@ -0,0 +1,19 @@ +function Get-YamlFingerprintHash { + <# + .SYNOPSIS + Hashes canonical YAML fingerprint input to a fixed-size digest. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Value, + + [Parameter(Mandatory)] + [System.Security.Cryptography.HashAlgorithm] $Hasher + ) + + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value) + return [System.Convert]::ToBase64String($Hasher.ComputeHash($bytes)) +} diff --git a/src/functions/private/Get-YamlNodeFingerprint.ps1 b/src/functions/private/Get-YamlNodeFingerprint.ps1 new file mode 100644 index 0000000..178eadb --- /dev/null +++ b/src/functions/private/Get-YamlNodeFingerprint.ps1 @@ -0,0 +1,78 @@ +function Get-YamlNodeFingerprint { + <# + .SYNOPSIS + Creates a structural fingerprint for YAML duplicate-key detection. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[int]] $Active, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.Dictionary[int, string]] $Cache, + + [Parameter(Mandatory)] + [System.Security.Cryptography.HashAlgorithm] $Hasher + ) + + if ($Node.Kind -eq 'Alias') { + return Get-YamlNodeFingerprint -Node $Node.Target -Active $Active -Cache $Cache -Hasher $Hasher + } + + $cachedFingerprint = '' + if ($Cache.TryGetValue($Node.Id, [ref] $cachedFingerprint)) { + return $cachedFingerprint + } + + if (-not $Active.Add($Node.Id)) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlCyclicMappingKey' -Message ( + 'A cyclic YAML node cannot be used as a mapping key.' + )) + } + + try { + if ($Node.Kind -eq 'Scalar') { + $resolved = Resolve-YamlScalar -Node $Node + $fingerprint = Get-YamlScalarFingerprint -Value $resolved -Hasher $Hasher + } elseif ($Node.Kind -eq 'Sequence') { + $parts = [System.Collections.Generic.List[string]]::new() + foreach ($item in $Node.Items) { + $itemFingerprint = Get-YamlNodeFingerprint -Node $item -Active $Active -Cache $Cache -Hasher $Hasher + $parts.Add($itemFingerprint) + } + $semanticTag = if ($Node.Tag -in @( + 'tag:yaml.org,2002:omap', + 'tag:yaml.org,2002:pairs' + )) { $Node.Tag } else { 'tag:yaml.org,2002:seq' } + $canonicalValue = 'sequence:{0}:{1}' -f $semanticTag, ($parts -join '|') + $fingerprint = Get-YamlFingerprintHash -Value $canonicalValue -Hasher $Hasher + } else { + $entries = [System.Collections.Generic.List[string]]::new() + foreach ($entry in $Node.Entries) { + $key = Get-YamlNodeFingerprint -Node $entry.Key -Active $Active -Cache $Cache -Hasher $Hasher + $entryValue = Get-YamlNodeFingerprint -Node $entry.Value -Active $Active -Cache $Cache -Hasher $Hasher + $entryFingerprint = Get-YamlFingerprintHash -Value "entry:$key=$entryValue" -Hasher $Hasher + $entries.Add($entryFingerprint) + } + $entries.Sort([System.StringComparer]::Ordinal) + $mappingTag = if ($Node.Tag -eq 'tag:yaml.org,2002:set') { + $Node.Tag + } else { + 'tag:yaml.org,2002:map' + } + $canonicalValue = 'mapping:{0}:{1}' -f $mappingTag, ($entries -join '|') + $fingerprint = Get-YamlFingerprintHash -Value $canonicalValue -Hasher $Hasher + } + + $Cache[$Node.Id] = $fingerprint + return $fingerprint + } finally { + [void] $Active.Remove($Node.Id) + } +} diff --git a/src/functions/private/Get-YamlScalarFingerprint.ps1 b/src/functions/private/Get-YamlScalarFingerprint.ps1 new file mode 100644 index 0000000..0eb73ab --- /dev/null +++ b/src/functions/private/Get-YamlScalarFingerprint.ps1 @@ -0,0 +1,46 @@ +function Get-YamlScalarFingerprint { + <# + .SYNOPSIS + Creates a canonical fingerprint for a resolved YAML scalar. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value, + + [Parameter(Mandatory)] + [System.Security.Cryptography.HashAlgorithm] $Hasher + ) + + if ($null -eq $Value) { + $canonicalValue = 'null' + } elseif ($Value -is [string]) { + $canonicalValue = 'str:{0}:{1}' -f $Value.Length, $Value + } elseif ($Value -is [bool]) { + $canonicalValue = 'bool:{0}' -f $Value.ToString().ToLowerInvariant() + } elseif ($Value -is [byte[]]) { + $canonicalValue = 'binary:{0}' -f [System.Convert]::ToBase64String($Value) + } elseif ($Value -is [datetimeoffset]) { + $canonicalValue = 'timestamp-offset:{0}' -f $Value.UtcDateTime.Ticks + } elseif ($Value -is [datetime]) { + $canonicalValue = 'timestamp:{0}' -f $Value.Ticks + } elseif ($Value -is [double]) { + if ([double]::IsNaN($Value)) { + $canonicalValue = 'float:nan' + } elseif ([double]::IsPositiveInfinity($Value)) { + $canonicalValue = 'float:+inf' + } elseif ([double]::IsNegativeInfinity($Value)) { + $canonicalValue = 'float:-inf' + } elseif ($Value -eq 0) { + $canonicalValue = 'float:0' + } else { + $canonicalValue = 'float:{0}' -f $Value.ToString('R', [cultureinfo]::InvariantCulture) + } + } else { + $canonicalValue = 'int:{0}' -f $Value.ToString([cultureinfo]::InvariantCulture) + } + + return Get-YamlFingerprintHash -Value $canonicalValue -Hasher $Hasher +} diff --git a/src/functions/private/New-YamlErrorRecord.ps1 b/src/functions/private/New-YamlErrorRecord.ps1 new file mode 100644 index 0000000..4f3f57c --- /dev/null +++ b/src/functions/private/New-YamlErrorRecord.ps1 @@ -0,0 +1,38 @@ +function New-YamlErrorRecord { + <# + .SYNOPSIS + Creates a public error record from an internal YAML exception. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Constructs an in-memory error record without changing system state.' + )] + [CmdletBinding()] + [OutputType([System.Management.Automation.ErrorRecord])] + param ( + [Parameter(Mandatory)] + [System.Exception] $Exception, + + [Parameter(Mandatory)] + [string] $DefaultErrorId, + + [Parameter(Mandatory)] + [System.Management.Automation.ErrorCategory] $Category, + + [AllowNull()] + [object] $TargetObject + ) + + $errorId = $DefaultErrorId + if ($Exception.Data.Contains('YamlErrorId')) { + $errorId = [string] $Exception.Data['YamlErrorId'] + } + + $record = [System.Management.Automation.ErrorRecord]::new( + $Exception, + $errorId, + $Category, + $TargetObject + ) + Write-Output -InputObject $record -NoEnumerate +} diff --git a/src/functions/private/New-YamlException.ps1 b/src/functions/private/New-YamlException.ps1 new file mode 100644 index 0000000..8c17a5c --- /dev/null +++ b/src/functions/private/New-YamlException.ps1 @@ -0,0 +1,34 @@ +function New-YamlException { + <# + .SYNOPSIS + Creates a location-aware YAML validation exception. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Constructs an in-memory exception without changing system state.' + )] + [CmdletBinding()] + [OutputType([YamlDotNet.Core.YamlException])] + param ( + [Parameter(Mandatory)] + [YamlDotNet.Core.Mark] $Start, + + [Parameter(Mandatory)] + [YamlDotNet.Core.Mark] $End, + + [Parameter(Mandatory)] + [string] $Message, + + [Parameter(Mandatory)] + [string] $ErrorId + ) + + $formattedMessage = '{0} Start: {1}. End: {2}.' -f @( + $Message.TrimEnd('.'), + $Start, + $End + ) + $exception = [YamlDotNet.Core.YamlException]::new($formattedMessage) + $exception.Data['YamlErrorId'] = $ErrorId + Write-Output -InputObject $exception -NoEnumerate +} diff --git a/src/functions/private/New-YamlNode.ps1 b/src/functions/private/New-YamlNode.ps1 new file mode 100644 index 0000000..37b837e --- /dev/null +++ b/src/functions/private/New-YamlNode.ps1 @@ -0,0 +1,44 @@ +function New-YamlNode { + <# + .SYNOPSIS + Creates an internal YAML representation node. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Constructs an in-memory representation node without changing system state.' + )] + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory)] + [int] $Id, + + [Parameter(Mandatory)] + [ValidateSet('Alias', 'Mapping', 'Scalar', 'Sequence')] + [string] $Kind, + + [Parameter(Mandatory)] + [YamlDotNet.Core.Mark] $Start, + + [Parameter(Mandatory)] + [YamlDotNet.Core.Mark] $End + ) + + $node = [pscustomobject]@{ + PSTypeName = 'PSModule.Yaml.InternalNode' + Id = $Id + Kind = $Kind + Tag = '' + Anchor = '' + Value = $null + Style = $null + IsPlainImplicit = $false + IsQuotedImplicit = $false + Items = [System.Collections.Generic.List[object]]::new() + Entries = [System.Collections.Generic.List[object]]::new() + Target = $null + Start = $Start + End = $End + } + Write-Output -InputObject $node -NoEnumerate +} diff --git a/src/functions/private/Read-YamlNode.ps1 b/src/functions/private/Read-YamlNode.ps1 new file mode 100644 index 0000000..902d2e1 --- /dev/null +++ b/src/functions/private/Read-YamlNode.ps1 @@ -0,0 +1,144 @@ +function Read-YamlNode { + <# + .SYNOPSIS + Reads one representation node from the low-level YAML event stream. + #> + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory)] + [YamlDotNet.Core.Parser] $Parser, + + [Parameter(Mandatory)] + [pscustomobject] $Context, + + [Parameter(Mandatory)] + [ValidateRange(1, 1024)] + [int] $Depth + ) + + $parserEvent = $Parser.Current + if ($null -eq $parserEvent) { + throw (New-YamlException -Start ([YamlDotNet.Core.Mark]::Empty) -End ([YamlDotNet.Core.Mark]::Empty) -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended while a node was expected.' + )) + } + if ($Depth -gt $Context.MaxDepth) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlDepthExceeded' -Message ( + "The YAML nesting depth exceeds the configured limit of $($Context.MaxDepth)." + )) + } + + $Context.NodeCount++ + if ($Context.NodeCount -gt $Context.MaxNodes) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlNodeLimitExceeded' -Message ( + "The YAML stream exceeds the configured limit of $($Context.MaxNodes) nodes." + )) + } + + $id = $Context.NextId + $Context.NextId++ + + if ($parserEvent -is [YamlDotNet.Core.Events.AnchorAlias]) { + $Context.AliasCount++ + if ($Context.AliasCount -gt $Context.MaxAliases) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlAliasLimitExceeded' -Message ( + "The YAML stream exceeds the configured limit of $($Context.MaxAliases) aliases." + )) + } + $anchorName = $parserEvent.Value.Value + if (-not $Context.Anchors.ContainsKey($anchorName)) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUndefinedAlias' -Message ( + "The YAML alias '*$anchorName' does not refer to a preceding anchor." + )) + } + $node = New-YamlNode -Id $id -Kind Alias -Start $parserEvent.Start -End $parserEvent.End + $node.Target = $Context.Anchors[$anchorName] + [void] $Parser.MoveNext() + Write-Output -InputObject $node -NoEnumerate + return + } + + if ($parserEvent -is [YamlDotNet.Core.Events.Scalar]) { + if ($parserEvent.Value.Length -gt $Context.MaxScalarLength) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlScalarLimitExceeded' -Message ( + "A YAML scalar exceeds the configured limit of $($Context.MaxScalarLength) characters." + )) + } + $node = New-YamlNode -Id $id -Kind Scalar -Start $parserEvent.Start -End $parserEvent.End + $node.Tag = $parserEvent.Tag.Value + $node.Anchor = $parserEvent.Anchor.Value + $node.Value = $parserEvent.Value + $node.Style = $parserEvent.Style + $node.IsPlainImplicit = $parserEvent.IsPlainImplicit + $node.IsQuotedImplicit = $parserEvent.IsQuotedImplicit + if (-not [string]::IsNullOrEmpty($node.Anchor)) { + $Context.Anchors[$node.Anchor] = $node + } + [void] $Parser.MoveNext() + Write-Output -InputObject $node -NoEnumerate + return + } + + if ($parserEvent -is [YamlDotNet.Core.Events.SequenceStart]) { + $node = New-YamlNode -Id $id -Kind Sequence -Start $parserEvent.Start -End $parserEvent.End + $node.Tag = $parserEvent.Tag.Value + $node.Anchor = $parserEvent.Anchor.Value + if (-not [string]::IsNullOrEmpty($node.Anchor)) { + $Context.Anchors[$node.Anchor] = $node + } + if (-not $Parser.MoveNext()) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended inside a sequence.' + )) + } + while ($Parser.Current -isnot [YamlDotNet.Core.Events.SequenceEnd]) { + $item = Read-YamlNode -Parser $Parser -Context $Context -Depth ($Depth + 1) + $node.Items.Add($item) + if ($null -eq $Parser.Current) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended inside a sequence.' + )) + } + } + $node.End = $Parser.Current.End + [void] $Parser.MoveNext() + Write-Output -InputObject $node -NoEnumerate + return + } + + if ($parserEvent -is [YamlDotNet.Core.Events.MappingStart]) { + $node = New-YamlNode -Id $id -Kind Mapping -Start $parserEvent.Start -End $parserEvent.End + $node.Tag = $parserEvent.Tag.Value + $node.Anchor = $parserEvent.Anchor.Value + if (-not [string]::IsNullOrEmpty($node.Anchor)) { + $Context.Anchors[$node.Anchor] = $node + } + if (-not $Parser.MoveNext()) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended inside a mapping.' + )) + } + while ($Parser.Current -isnot [YamlDotNet.Core.Events.MappingEnd]) { + $key = Read-YamlNode -Parser $Parser -Context $Context -Depth ($Depth + 1) + $entryValue = Read-YamlNode -Parser $Parser -Context $Context -Depth ($Depth + 1) + $node.Entries.Add([pscustomobject]@{ + Key = $key + Value = $entryValue + }) + if ($null -eq $Parser.Current) { + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended inside a mapping.' + )) + } + } + $node.End = $Parser.Current.End + [void] $Parser.MoveNext() + Write-Output -InputObject $node -NoEnumerate + return + } + + throw (New-YamlException -Start $parserEvent.Start -End $parserEvent.End -ErrorId 'YamlUnexpectedEvent' -Message ( + "Unexpected YAML parser event '$($parserEvent.GetType().Name)'." + )) +} diff --git a/src/functions/private/Read-YamlStream.ps1 b/src/functions/private/Read-YamlStream.ps1 new file mode 100644 index 0000000..71f709d --- /dev/null +++ b/src/functions/private/Read-YamlStream.ps1 @@ -0,0 +1,64 @@ +function Read-YamlStream { + <# + .SYNOPSIS + Parses YAML text with a narrow parser-compatibility retry. + #> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Yaml, + + [Parameter(Mandatory)] + [ValidateRange(1, 1024)] + [int] $Depth, + + [Parameter(Mandatory)] + [ValidateRange(1, 2147483647)] + [int] $MaxNodes, + + [Parameter(Mandatory)] + [ValidateRange(0, 2147483647)] + [int] $MaxAliases, + + [Parameter(Mandatory)] + [ValidateRange(1, 2147483647)] + [int] $MaxScalarLength + ) + + try { + Write-Output -InputObject ( + Read-YamlStreamCore -Yaml $Yaml -Depth $Depth -MaxNodes $MaxNodes ` + -MaxAliases $MaxAliases -MaxScalarLength $MaxScalarLength + ) -NoEnumerate + } catch [YamlDotNet.Core.SemanticErrorException] { + $compatibleYaml = ConvertTo-YamlParserCompatibleText -Yaml $Yaml + if ($compatibleYaml -ceq $Yaml) { + throw + } + Write-Output -InputObject ( + Read-YamlStreamCore -Yaml $compatibleYaml -Depth $Depth -MaxNodes $MaxNodes ` + -MaxAliases $MaxAliases -MaxScalarLength $MaxScalarLength + ) -NoEnumerate + } catch [System.Management.Automation.MethodInvocationException] { + if ($_.Exception.InnerException -is [YamlDotNet.Core.SemanticErrorException]) { + $compatibleYaml = ConvertTo-YamlParserCompatibleText -Yaml $Yaml + if ($compatibleYaml -ceq $Yaml) { + throw $_.Exception.InnerException + } + Write-Output -InputObject ( + Read-YamlStreamCore -Yaml $compatibleYaml -Depth $Depth -MaxNodes $MaxNodes ` + -MaxAliases $MaxAliases -MaxScalarLength $MaxScalarLength + ) -NoEnumerate + return + } + if ($_.Exception.InnerException -isnot [System.InvalidOperationException]) { + throw + } + throw (New-YamlException -Start ([YamlDotNet.Core.Mark]::Empty) ` + -End ([YamlDotNet.Core.Mark]::Empty) -ErrorId 'YamlInvalidSyntax' -Message ( + 'YamlDotNet entered an invalid parser state while reading malformed YAML.' + )) + } +} diff --git a/src/functions/private/Read-YamlStreamCore.ps1 b/src/functions/private/Read-YamlStreamCore.ps1 new file mode 100644 index 0000000..3c5e1a4 --- /dev/null +++ b/src/functions/private/Read-YamlStreamCore.ps1 @@ -0,0 +1,90 @@ +function Read-YamlStreamCore { + <# + .SYNOPSIS + Reads and validates all documents in a YAML stream. + #> + [CmdletBinding()] + [OutputType([object[]])] + param ( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Yaml, + + [Parameter(Mandatory)] + [ValidateRange(1, 1024)] + [int] $Depth, + + [Parameter(Mandatory)] + [ValidateRange(1, 2147483647)] + [int] $MaxNodes, + + [Parameter(Mandatory)] + [ValidateRange(0, 2147483647)] + [int] $MaxAliases, + + [Parameter(Mandatory)] + [ValidateRange(1, 2147483647)] + [int] $MaxScalarLength + ) + + $parser = [YamlDotNet.Core.Parser]::new([System.IO.StringReader]::new($Yaml)) + $documents = [System.Collections.Generic.List[object]]::new() + $context = [pscustomobject]@{ + NextId = 1 + NodeCount = 0 + AliasCount = 0 + MaxDepth = $Depth + MaxNodes = $MaxNodes + MaxAliases = $MaxAliases + MaxScalarLength = $MaxScalarLength + Anchors = $null + } + + if (-not $parser.MoveNext() -or $parser.Current -isnot [YamlDotNet.Core.Events.StreamStart]) { + throw (New-YamlException -Start ([YamlDotNet.Core.Mark]::Empty) -End ([YamlDotNet.Core.Mark]::Empty) -ErrorId 'YamlInvalidStream' -Message ( + 'The input is not a valid YAML stream.' + )) + } + [void] $parser.MoveNext() + + while ($parser.Current -is [YamlDotNet.Core.Events.DocumentStart]) { + $context.Anchors = [System.Collections.Generic.Dictionary[string, object]]::new( + [System.StringComparer]::Ordinal + ) + if (-not $parser.MoveNext()) { + $emptyMark = [YamlDotNet.Core.Mark]::Empty + $exception = New-YamlException -Start $emptyMark -End $emptyMark -ErrorId 'YamlUnexpectedEnd' -Message ( + 'The YAML stream ended after a document start.' + ) + throw $exception + } + + $document = Read-YamlNode -Parser $parser -Context $context -Depth 1 + if ($parser.Current -isnot [YamlDotNet.Core.Events.DocumentEnd]) { + $mark = if ($null -eq $parser.Current) { [YamlDotNet.Core.Mark]::Empty } else { $parser.Current.Start } + throw (New-YamlException -Start $mark -End $mark -ErrorId 'YamlInvalidDocument' -Message ( + 'The YAML document did not end where expected.' + )) + } + + $fingerprintHasher = [System.Security.Cryptography.SHA256]::Create() + try { + Test-YamlNodeGraph -Node $document -Visited ([System.Collections.Generic.HashSet[int]]::new()) ` + -FingerprintCache ([System.Collections.Generic.Dictionary[int, string]]::new()) ` + -FingerprintHasher $fingerprintHasher + } finally { + $fingerprintHasher.Dispose() + } + $documents.Add($document) + [void] $parser.MoveNext() + } + + if ($parser.Current -isnot [YamlDotNet.Core.Events.StreamEnd]) { + $mark = if ($null -eq $parser.Current) { [YamlDotNet.Core.Mark]::Empty } else { $parser.Current.Start } + throw (New-YamlException -Start $mark -End $mark -ErrorId 'YamlInvalidStream' -Message ( + 'The YAML stream contains unexpected content.' + )) + } + + Write-Output -InputObject ([object[]] $documents.ToArray()) -NoEnumerate +} diff --git a/src/functions/private/Resolve-YamlScalar.ps1 b/src/functions/private/Resolve-YamlScalar.ps1 new file mode 100644 index 0000000..68ef51c --- /dev/null +++ b/src/functions/private/Resolve-YamlScalar.ps1 @@ -0,0 +1,159 @@ +function Resolve-YamlScalar { + <# + .SYNOPSIS + Constructs a safe PowerShell scalar using YAML 1.2 schema rules. + #> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node + ) + + $tagPrefix = 'tag:yaml.org,2002:' + $tag = [string] $Node.Tag + $value = [string] $Node.Value + $isImplicit = [string]::IsNullOrEmpty($tag) -and $Node.IsPlainImplicit + + if (-not $isImplicit -and -not $tag.StartsWith($tagPrefix, [System.StringComparison]::Ordinal)) { + Write-Output -InputObject $value -NoEnumerate + return + } + + $standardTag = if ($isImplicit) { '' } else { $tag.Substring($tagPrefix.Length) } + $nullPattern = '^(?:|~|null|Null|NULL)$' + $booleanPattern = '^(?:true|True|TRUE|false|False|FALSE)$' + $integerPattern = '^(?:[-+]?[0-9]+|0o[0-7]+|0x[0-9a-fA-F]+)$' + $numberPattern = '^[-+]?(?:(?:\.[0-9]+)|(?:[0-9]+(?:\.[0-9]*)?))(?:[eE][-+]?[0-9]+)?$' + $infinityPattern = '^[-+]?\.(?:inf|Inf|INF)$' + $nanPattern = '^\.(?:nan|NaN|NAN)$' + + if ($standardTag -eq 'str') { + Write-Output -InputObject $value -NoEnumerate + return + } + + if (($standardTag -eq 'null' -or $isImplicit) -and $value -cmatch $nullPattern) { + return + } + + if (($standardTag -eq 'bool' -or $isImplicit) -and $value -cmatch $booleanPattern) { + Write-Output -InputObject ($value[0] -ceq 't' -or $value[0] -ceq 'T') -NoEnumerate + return + } + + if (($standardTag -eq 'int' -or $isImplicit) -and $value -cmatch $integerPattern) { + Write-Output -InputObject (ConvertFrom-YamlInteger -Value $value) -NoEnumerate + return + } + + if (($standardTag -eq 'float' -or $isImplicit) -and $value -cmatch $infinityPattern) { + $infinity = if ($value.StartsWith('-', [System.StringComparison]::Ordinal)) { + [double]::NegativeInfinity + } else { + [double]::PositiveInfinity + } + Write-Output -InputObject $infinity -NoEnumerate + return + } + + if (($standardTag -eq 'float' -or $isImplicit) -and $value -cmatch $nanPattern) { + Write-Output -InputObject ([double]::NaN) -NoEnumerate + return + } + + if (($standardTag -eq 'float' -or $isImplicit) -and $value -cmatch $numberPattern) { + $number = [double] 0 + $parsed = [double]::TryParse( + $value, + [System.Globalization.NumberStyles]::Float, + [System.Globalization.CultureInfo]::InvariantCulture, + [ref] $number + ) + if (-not $parsed -or [double]::IsInfinity($number)) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlFloatOverflow' -Message ( + "The finite YAML floating-point value '$value' is outside the supported range." + )) + } + Write-Output -InputObject $number -NoEnumerate + return + } + + if ($standardTag -eq 'binary') { + try { + $bytes = [System.Convert]::FromBase64String(($value -replace '\s', '')) + } catch [System.FormatException] { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlInvalidBinary' -Message ( + "The value for tag 'tag:yaml.org,2002:binary' is not valid Base64." + )) + } + Write-Output -InputObject $bytes -NoEnumerate + return + } + + if ($standardTag -eq 'timestamp') { + $timestampPattern = '^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[ \t]*(?:[Zz]|[-+]\d{1,2}(?::?\d{2})?))?)?$' + if ($value -cnotmatch $timestampPattern) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlInvalidTimestamp' -Message ( + "The value '$value' is not a supported YAML timestamp." + )) + } + + $date = [datetime]::MinValue + if ([datetime]::TryParseExact( + $value, + 'yyyy-MM-dd', + [System.Globalization.CultureInfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::None, + [ref] $date + )) { + Write-Output -InputObject ([datetime]::SpecifyKind($date, [System.DateTimeKind]::Unspecified)) -NoEnumerate + return + } + + $normalized = $value -replace '\s+([+-]\d{1,2}(?::?\d{2})?)$', '$1' + if ($normalized -match '([+-])(\d{1,2})$') { + $offsetSuffix = '{0}{1}:00' -f $Matches[1], $Matches[2].PadLeft(2, '0') + $normalized = $normalized.Substring(0, $normalized.Length - $Matches[0].Length) + $offsetSuffix + } elseif ($normalized -match '([+-])(\d{2})(\d{2})$') { + $offsetSuffix = '{0}{1}:{2}' -f $Matches[1], $Matches[2], $Matches[3] + $normalized = $normalized.Substring(0, $normalized.Length - $Matches[0].Length) + $offsetSuffix + } + + if ($normalized -cmatch '(?:[Zz]|[-+]\d{2}:\d{2})$') { + $offset = [datetimeoffset]::MinValue + if ([datetimeoffset]::TryParse( + $normalized, + [System.Globalization.CultureInfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::AllowWhiteSpaces, + [ref] $offset + )) { + Write-Output -InputObject $offset -NoEnumerate + return + } + } else { + $dateTime = [datetime]::MinValue + if ([datetime]::TryParse( + $normalized, + [System.Globalization.CultureInfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::AllowWhiteSpaces, + [ref] $dateTime + )) { + Write-Output -InputObject ([datetime]::SpecifyKind($dateTime, [System.DateTimeKind]::Unspecified)) -NoEnumerate + return + } + } + + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlInvalidTimestamp' -Message ( + "The value '$value' is not a supported YAML timestamp." + )) + } + + if (-not $isImplicit -and $standardTag -in @('null', 'bool', 'int', 'float')) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlInvalidTaggedScalar' -Message ( + "The value '$value' is invalid for YAML tag '$tag'." + )) + } + + Write-Output -InputObject $value -NoEnumerate +} diff --git a/src/functions/private/Test-YamlNodeGraph.ps1 b/src/functions/private/Test-YamlNodeGraph.ps1 new file mode 100644 index 0000000..2672e5e --- /dev/null +++ b/src/functions/private/Test-YamlNodeGraph.ps1 @@ -0,0 +1,142 @@ +function Test-YamlNodeGraph { + <# + .SYNOPSIS + Validates tags and mapping-key uniqueness in a YAML node graph. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[int]] $Visited, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.Dictionary[int, string]] $FingerprintCache, + + [Parameter(Mandatory)] + [System.Security.Cryptography.HashAlgorithm] $FingerprintHasher + ) + + if ($Node.Kind -eq 'Alias') { + Test-YamlNodeGraph -Node $Node.Target -Visited $Visited -FingerprintCache $FingerprintCache ` + -FingerprintHasher $FingerprintHasher + return + } + if (-not $Visited.Add($Node.Id)) { + return + } + + $scalarTags = @( + 'tag:yaml.org,2002:binary', + 'tag:yaml.org,2002:bool', + 'tag:yaml.org,2002:float', + 'tag:yaml.org,2002:int', + 'tag:yaml.org,2002:null', + 'tag:yaml.org,2002:str', + 'tag:yaml.org,2002:timestamp' + ) + $tag = [string] $Node.Tag + + if ($Node.Kind -eq 'Scalar') { + if ($tag -in @( + 'tag:yaml.org,2002:map', + 'tag:yaml.org,2002:omap', + 'tag:yaml.org,2002:pairs', + 'tag:yaml.org,2002:seq', + 'tag:yaml.org,2002:set' + )) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlTagKindMismatch' -Message ( + "YAML tag '$tag' cannot be applied to a scalar node." + )) + } + $null = Resolve-YamlScalar -Node $Node + return + } + + if ($tag -in $scalarTags) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlTagKindMismatch' -Message ( + "YAML tag '$tag' cannot be applied to a $($Node.Kind.ToLowerInvariant()) node." + )) + } + + if ($Node.Kind -eq 'Sequence') { + if ($tag -in @('tag:yaml.org,2002:map', 'tag:yaml.org,2002:set')) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlTagKindMismatch' -Message ( + "YAML tag '$tag' requires a mapping node." + )) + } + + $isPairs = $tag -eq 'tag:yaml.org,2002:pairs' + $isOrderedMap = $tag -eq 'tag:yaml.org,2002:omap' + $orderedKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($item in $Node.Items) { + Test-YamlNodeGraph -Node $item -Visited $Visited -FingerprintCache $FingerprintCache ` + -FingerprintHasher $FingerprintHasher + if ($isPairs -or $isOrderedMap) { + $entryNode = $item + while ($entryNode.Kind -eq 'Alias') { + $entryNode = $entryNode.Target + } + if ($entryNode.Kind -ne 'Mapping' -or $entryNode.Entries.Count -ne 1) { + throw (New-YamlException -Start $item.Start -End $item.End -ErrorId 'YamlInvalidTaggedCollection' -Message ( + "YAML tag '$tag' requires a sequence of one-entry mappings." + )) + } + if ($isOrderedMap) { + $keyFingerprint = Get-YamlNodeFingerprint -Node $entryNode.Entries[0].Key -Active ( + [System.Collections.Generic.HashSet[int]]::new() + ) -Cache $FingerprintCache -Hasher $FingerprintHasher + if (-not $orderedKeys.Add($keyFingerprint)) { + $keyNode = $entryNode.Entries[0].Key + $exception = New-YamlException -Start $keyNode.Start -End $keyNode.End ` + -ErrorId 'YamlDuplicateKey' -Message 'A duplicate key was found in a YAML ordered mapping.' + throw $exception + } + } + } + } + return + } + + if ($tag -in @( + 'tag:yaml.org,2002:omap', + 'tag:yaml.org,2002:pairs', + 'tag:yaml.org,2002:seq' + )) { + throw (New-YamlException -Start $Node.Start -End $Node.End -ErrorId 'YamlTagKindMismatch' -Message ( + "YAML tag '$tag' requires a sequence node." + )) + } + + $keys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($entry in $Node.Entries) { + $fingerprint = Get-YamlNodeFingerprint -Node $entry.Key -Active ( + [System.Collections.Generic.HashSet[int]]::new() + ) -Cache $FingerprintCache -Hasher $FingerprintHasher + if (-not $keys.Add($fingerprint)) { + throw (New-YamlException -Start $entry.Key.Start -End $entry.Key.End -ErrorId 'YamlDuplicateKey' -Message ( + 'A duplicate mapping key is not allowed.' + )) + } + + Test-YamlNodeGraph -Node $entry.Key -Visited $Visited -FingerprintCache $FingerprintCache ` + -FingerprintHasher $FingerprintHasher + Test-YamlNodeGraph -Node $entry.Value -Visited $Visited -FingerprintCache $FingerprintCache ` + -FingerprintHasher $FingerprintHasher + + if ($tag -eq 'tag:yaml.org,2002:set') { + $setValue = $entry.Value + while ($setValue.Kind -eq 'Alias') { + $setValue = $setValue.Target + } + if ($setValue.Kind -ne 'Scalar' -or $null -ne (Resolve-YamlScalar -Node $setValue)) { + throw (New-YamlException -Start $entry.Value.Start -End $entry.Value.End -ErrorId 'YamlInvalidTaggedCollection' -Message ( + 'Every value in a YAML set must be null.' + )) + } + } + } +} diff --git a/src/functions/public/ConvertFrom-Yaml.ps1 b/src/functions/public/ConvertFrom-Yaml.ps1 new file mode 100644 index 0000000..632a145 --- /dev/null +++ b/src/functions/public/ConvertFrom-Yaml.ps1 @@ -0,0 +1,117 @@ +function ConvertFrom-Yaml { + <# + .SYNOPSIS + Converts a YAML stream into PowerShell values. + + .DESCRIPTION + Parses YAML with YamlDotNet's low-level parser and constructs values + with the YAML 1.2 core schema. Mapping keys must be unique. Unknown + application tags never activate .NET types and are treated as neutral + metadata. + + Pipeline strings are joined with a line feed and parsed as one stream, + which supports Get-Content. Each YAML document is written separately. + + .PARAMETER Yaml + YAML text. Multiple pipeline records are joined with a line feed and + parsed as one YAML stream. + + .PARAMETER AsHashtable + Returns mappings as insertion-ordered dictionaries. Use this for + complex, non-string, empty, or case-colliding mapping keys. + + .PARAMETER NoEnumerate + Writes each top-level YAML sequence as one array pipeline record. + + .PARAMETER Depth + Maximum YAML node nesting depth. The default is 100. + + .PARAMETER MaxNodes + Maximum number of YAML nodes in the stream. The default is 100000. + + .PARAMETER MaxAliases + Maximum number of alias nodes in the stream. The default is 1000. + + .PARAMETER MaxScalarLength + Maximum decoded character count for one scalar. The default is + 1048576. + + .EXAMPLE + 'name: Ada' | ConvertFrom-Yaml + + Converts one mapping to a PSCustomObject. + + .EXAMPLE + Get-Content -Path '.\config.yaml' | ConvertFrom-Yaml -AsHashtable + + Joins the input lines and returns ordered dictionaries. + + .INPUTS + System.String[] + + .OUTPUTS + System.Object + #> + [CmdletBinding()] + [OutputType([object])] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowEmptyString()] + [string[]] $Yaml, + + [switch] $AsHashtable, + + [switch] $NoEnumerate, + + [ValidateRange(1, 1024)] + [int] $Depth = 100, + + [ValidateRange(1, 2147483647)] + [int] $MaxNodes = 100000, + + [ValidateRange(0, 2147483647)] + [int] $MaxAliases = 1000, + + [ValidateRange(1, 2147483647)] + [int] $MaxScalarLength = 1048576 + ) + + begin { + $lines = [System.Collections.Generic.List[string]]::new() + } + process { + foreach ($line in $Yaml) { + $lines.Add($line) + } + } + end { + $yamlText = $lines -join "`n" + try { + $documents = Read-YamlStream -Yaml $yamlText -Depth $Depth -MaxNodes $MaxNodes ` + -MaxAliases $MaxAliases -MaxScalarLength $MaxScalarLength + foreach ($document in $documents) { + $value = ConvertFrom-YamlNode -Node $document -AsHashtable:$AsHashtable -Cache ( + [System.Collections.Generic.Dictionary[int, object]]::new() + ) + + $effectiveNode = $document + while ($effectiveNode.Kind -eq 'Alias') { + $effectiveNode = $effectiveNode.Target + } + $isTopLevelSequence = $effectiveNode.Kind -eq 'Sequence' -and $effectiveNode.Tag -ne 'tag:yaml.org,2002:omap' + + if ($isTopLevelSequence -and -not $NoEnumerate) { + foreach ($item in $value) { + $PSCmdlet.WriteObject($item, $false) + } + } else { + $PSCmdlet.WriteObject($value, $false) + } + } + } catch [YamlDotNet.Core.YamlException] { + $record = New-YamlErrorRecord -Exception $_.Exception -DefaultErrorId 'YamlInvalidInput' ` + -Category InvalidData -TargetObject $yamlText + $PSCmdlet.ThrowTerminatingError($record) + } + } +} diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 deleted file mode 100644 index ffe3483..0000000 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -#Requires -Modules Utilities - -function Get-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/Test-Yaml.ps1 b/src/functions/public/Test-Yaml.ps1 new file mode 100644 index 0000000..36a913e --- /dev/null +++ b/src/functions/public/Test-Yaml.ps1 @@ -0,0 +1,78 @@ +function Test-Yaml { + <# + .SYNOPSIS + Tests whether text is a valid, safely processable YAML stream. + + .DESCRIPTION + Parses a YAML stream, checks unique mapping keys, validates standard + tags, and applies the same resource limits as ConvertFrom-Yaml. It + returns false only for YamlDotNet YAML exceptions. Unexpected runtime + failures are not swallowed. + + Pipeline strings are joined with a line feed and tested as one stream. + + .PARAMETER Yaml + YAML text. Multiple pipeline records are joined with a line feed. + + .PARAMETER Depth + Maximum YAML node nesting depth. The default is 100. + + .PARAMETER MaxNodes + Maximum number of YAML nodes in the stream. The default is 100000. + + .PARAMETER MaxAliases + Maximum number of alias nodes in the stream. The default is 1000. + + .PARAMETER MaxScalarLength + Maximum decoded character count for one scalar. The default is + 1048576. + + .EXAMPLE + 'name: Ada' | Test-Yaml + + Returns true. + + .INPUTS + System.String[] + + .OUTPUTS + System.Boolean + #> + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowEmptyString()] + [string[]] $Yaml, + + [ValidateRange(1, 1024)] + [int] $Depth = 100, + + [ValidateRange(1, 2147483647)] + [int] $MaxNodes = 100000, + + [ValidateRange(0, 2147483647)] + [int] $MaxAliases = 1000, + + [ValidateRange(1, 2147483647)] + [int] $MaxScalarLength = 1048576 + ) + + begin { + $lines = [System.Collections.Generic.List[string]]::new() + } + process { + foreach ($line in $Yaml) { + $lines.Add($line) + } + } + end { + try { + $null = Read-YamlStream -Yaml ($lines -join "`n") -Depth $Depth -MaxNodes $MaxNodes ` + -MaxAliases $MaxAliases -MaxScalarLength $MaxScalarLength + $PSCmdlet.WriteObject($true) + } catch [YamlDotNet.Core.YamlException] { + $PSCmdlet.WriteObject($false) + } + } +} diff --git a/tests/ConvertFrom-Yaml.Tests.ps1 b/tests/ConvertFrom-Yaml.Tests.ps1 new file mode 100644 index 0000000..f260aac --- /dev/null +++ b/tests/ConvertFrom-Yaml.Tests.ps1 @@ -0,0 +1,259 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +BeforeAll { + . (Join-Path $PSScriptRoot 'TestBootstrap.ps1') +} + +Describe 'ConvertFrom-Yaml' { + Context 'YAML 1.2 core schema' { + It 'resolves as ' -ForEach @( + @{ Text = ''; Type = 'null'; Expected = $null } + @{ Text = '~'; Type = 'null'; Expected = $null } + @{ Text = 'null'; Type = 'null'; Expected = $null } + @{ Text = 'Null'; Type = 'null'; Expected = $null } + @{ Text = 'NULL'; Type = 'null'; Expected = $null } + @{ Text = 'true'; Type = 'Boolean'; Expected = $true } + @{ Text = 'True'; Type = 'Boolean'; Expected = $true } + @{ Text = 'TRUE'; Type = 'Boolean'; Expected = $true } + @{ Text = 'false'; Type = 'Boolean'; Expected = $false } + @{ Text = 'False'; Type = 'Boolean'; Expected = $false } + @{ Text = 'FALSE'; Type = 'Boolean'; Expected = $false } + @{ Text = '01'; Type = 'Int32'; Expected = 1 } + @{ Text = '0o14'; Type = 'Int32'; Expected = 12 } + @{ Text = '0xC'; Type = 'Int32'; Expected = 12 } + @{ Text = '1.23015e+3'; Type = 'Double'; Expected = 1230.15 } + ) { + $result = "value: $Text" | ConvertFrom-Yaml + + $result.value | Should -Be $Expected + if ($Type -ne 'null') { + $result.value.GetType().Name | Should -Be $Type + } + } + + It 'keeps YAML 1.1-only implicit values and timestamps as strings' { + $result = @' +yes: yes +no: NO +on: on +off: Off +timestamp: 2001-12-15T02:59:43.1Z +'@ | ConvertFrom-Yaml + + $result.yes | Should -BeOfType [string] + $result.no | Should -BeOfType [string] + $result.on | Should -BeOfType [string] + $result.off | Should -BeOfType [string] + $result.timestamp | Should -BeOfType [string] + } + + It 'keeps quoted and block scalars as strings' { + $result = @' +quoted: "true" +literal: | + 42 +folded: > + null + text +'@ | ConvertFrom-Yaml + + $result.quoted | Should -Be 'true' + $result.quoted | Should -BeOfType [string] + $result.literal | Should -Be "42`n" + $result.folded | Should -Be 'null text' + } + + It 'uses BigInteger beyond Int64' { + $result = 'value: 9223372036854775808' | ConvertFrom-Yaml + + $result.value | Should -BeOfType [System.Numerics.BigInteger] + $result.value.ToString() | Should -Be '9223372036854775808' + } + } + + Context 'Mappings and sequences' { + It 'returns ordinary mappings as ordered PSCustomObject properties' { + $result = "zebra: 1`napple: 2" | ConvertFrom-Yaml + + $result | Should -BeOfType [pscustomobject] + @($result.PSObject.Properties.Name) | Should -Be @('zebra', 'apple') + } + + It 'returns recursive ordered dictionaries with AsHashtable' { + $result = "outer:`n inner: value" | ConvertFrom-Yaml -AsHashtable + + $result | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result['outer'] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $result['outer']['inner'] | Should -Be 'value' + } + + It 'preserves a complex key with AsHashtable' { + $result = "? [Detroit Tigers, Chicago Cubs]`n: 2001-07-23" | + ConvertFrom-Yaml -AsHashtable + $enumerator = $result.GetEnumerator() + $null = $enumerator.MoveNext() + $key = $enumerator.Key + + , $key | Should -BeOfType [object[]] + $key | Should -Be @('Detroit Tigers', 'Chicago Cubs') + $enumerator.Value | Should -Be '2001-07-23' + } + + It 'fails rather than losing a complex key in PSCustomObject mode' { + { "? [a, b]`n: value" | ConvertFrom-Yaml } | + Should -Throw -ExpectedMessage '*Use -AsHashtable*' + } + + It 'fails on case-insensitive property collisions but preserves them with AsHashtable' { + { "Name: one`nname: two" | ConvertFrom-Yaml } | + Should -Throw -ExpectedMessage '*case-insensitive property collision*' + + $result = "Name: one`nname: two" | ConvertFrom-Yaml -AsHashtable + $result.Count | Should -Be 2 + $result['Name'] | Should -Be 'one' + $result['name'] | Should -Be 'two' + } + + It 'enumerates only top-level sequences by default' { + $result = "- one`n- two" | ConvertFrom-Yaml + + @($result) | Should -Be @('one', 'two') + } + + It 'preserves a top-level sequence with NoEnumerate' { + $result = "- one`n- two" | ConvertFrom-Yaml -NoEnumerate + + , $result | Should -BeOfType [object[]] + $result | Should -Be @('one', 'two') + } + } + + Context 'Streams and pipeline input' { + It 'joins pipeline lines as one YAML stream' { + $result = 'name: Ada', 'active: true' | ConvertFrom-Yaml + + $result.name | Should -Be 'Ada' + $result.active | Should -BeTrue + } + + It 'returns every document separately' { + $result = @( + "---`nname: first`n...`n---`nname: second" | ConvertFrom-Yaml + ) + + $result.Count | Should -Be 2 + $result[0].name | Should -Be 'first' + $result[1].name | Should -Be 'second' + } + } + + Context 'Tags, anchors, and aliases' { + It 'constructs explicit standard scalar tags safely' { + $result = @' +text: !!str 42 +number: !!int "42" +binary: !!binary SGVsbG8= +offset: !!timestamp 2026-07-19T15:49:21+02:00 +date: !!timestamp 2026-07-19 +'@ | ConvertFrom-Yaml + + $result.text | Should -BeOfType [string] + $result.number | Should -BeOfType [int] + [Text.Encoding]::UTF8.GetString($result.binary) | Should -Be 'Hello' + $result.offset | Should -BeOfType [datetimeoffset] + $result.date | Should -BeOfType [datetime] + } + + It 'treats unknown application tags as neutral non-activating metadata' { + $result = @' +scalar: !System.Management.Automation.PSObject 42 +mapping: ! + name: safe +'@ | ConvertFrom-Yaml + + $result.scalar | Should -Be '42' + $result.scalar | Should -BeOfType [string] + $result.mapping.name | Should -Be 'safe' + } + + It 'preserves repeated collection references' { + $result = @' +source: &source + value: 1 +copy: *source +'@ | ConvertFrom-Yaml + + [object]::ReferenceEquals($result.source, $result.copy) | Should -BeTrue + } + + It 'constructs recursive aliases without recursing forever' { + $result = '&root [*root]' | ConvertFrom-Yaml -NoEnumerate + + [object]::ReferenceEquals($result, $result[0]) | Should -BeTrue + } + + It 'constructs set, ordered-map, and pairs tags safely' { + $set = "!!set`n? one`n? two" | ConvertFrom-Yaml + $orderedMap = "!!omap`n- one: 1`n- two: 2" | ConvertFrom-Yaml + $pairs = "!!pairs`n- one: 1`n- one: 2" | ConvertFrom-Yaml -NoEnumerate + + $set | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + $set.Count | Should -Be 2 + $orderedMap | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + @($orderedMap.Keys) | Should -Be @('one', 'two') + $pairs.Count | Should -Be 2 + $pairs[0] | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + } + } + + Context 'Validation and limits' { + It 'rejects duplicate scalar, canonical numeric, and complex keys' { + { "key: one`nkey: two" | ConvertFrom-Yaml } | Should -Throw + { "1: one`n01: two" | ConvertFrom-Yaml -AsHashtable } | Should -Throw + { "? [a, b]`n: one`n? [a, b]`n: two" | ConvertFrom-Yaml -AsHashtable } | + Should -Throw + { "? {a: 1, A: 1}`n: one`n? {A: 1, a: 1}`n: two" | ConvertFrom-Yaml -AsHashtable } | + Should -Throw + } + + It 'rejects equivalent offset timestamps as duplicate keys' { + $yaml = @' +? !!timestamp 2001-12-15T02:59:43.1Z +: one +? !!timestamp 2001-12-14T21:59:43.1-05:00 +: two +'@ + + { $yaml | ConvertFrom-Yaml -AsHashtable } | Should -Throw + ($yaml | Test-Yaml) | Should -BeFalse + } + + It 'rejects finite floating-point values outside the supported range' { + { 'value: 1e9999' | ConvertFrom-Yaml } | + Should -Throw -ExpectedMessage '*outside the supported range*' + ('value: 1e9999' | Test-Yaml) | Should -BeFalse + } + + It 'rejects undefined aliases' { + { 'value: *missing' | ConvertFrom-Yaml } | Should -Throw + } + + It 'enforces depth, node, alias, and scalar limits' { + { "a:`n b:`n c: value" | ConvertFrom-Yaml -Depth 2 } | Should -Throw + { "[one, two]" | ConvertFrom-Yaml -MaxNodes 2 } | Should -Throw + { "a: &a value`nb: *a" | ConvertFrom-Yaml -MaxAliases 0 } | Should -Throw + { 'value: long' | ConvertFrom-Yaml -MaxScalarLength 4 } | Should -Throw + } + } +} diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 deleted file mode 100644 index 8258bb7..0000000 --- a/tests/PSModuleTest.Tests.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Module' { - It 'Function: Get-PSModuleTest' { - Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } -} diff --git a/tests/Specification.Tests.ps1 b/tests/Specification.Tests.ps1 new file mode 100644 index 0000000..f316a65 --- /dev/null +++ b/tests/Specification.Tests.ps1 @@ -0,0 +1,206 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +BeforeAll { + . (Join-Path $PSScriptRoot 'TestBootstrap.ps1') +} + +Describe 'YAML 1.2.2 Chapter 2 examples' { + BeforeAll { + $chapterPath = Join-Path $PSScriptRoot 'fixtures\yaml-spec-1.2.2\chapter-02' + } + + It 'accepts and constructs Example : ' -ForEach @( + @{ Number = '2.01'; Name = 'Sequence of Scalars' } + @{ Number = '2.02'; Name = 'Mapping Scalars to Scalars' } + @{ Number = '2.03'; Name = 'Mapping Scalars to Sequences' } + @{ Number = '2.04'; Name = 'Sequence of Mappings' } + @{ Number = '2.05'; Name = 'Sequence of Sequences' } + @{ Number = '2.06'; Name = 'Mapping of Mappings' } + @{ Number = '2.07'; Name = 'Two Documents in a Stream' } + @{ Number = '2.08'; Name = 'Play by Play Feed' } + @{ Number = '2.09'; Name = 'Document with Comments' } + @{ Number = '2.10'; Name = 'Anchor and Alias' } + @{ Number = '2.11'; Name = 'Mapping between Sequences' } + @{ Number = '2.12'; Name = 'Compact Nested Mapping' } + @{ Number = '2.13'; Name = 'Literal Scalar' } + @{ Number = '2.14'; Name = 'Folded Scalar' } + @{ Number = '2.15'; Name = 'Folded More-indented Lines' } + @{ Number = '2.16'; Name = 'Indentation Scope' } + @{ Number = '2.17'; Name = 'Quoted Scalars' } + @{ Number = '2.18'; Name = 'Multi-line Flow Scalars' } + @{ Number = '2.19'; Name = 'Integers' } + @{ Number = '2.20'; Name = 'Floating Point' } + @{ Number = '2.21'; Name = 'Miscellaneous Scalars' } + @{ Number = '2.22'; Name = 'Timestamps' } + @{ Number = '2.23'; Name = 'Explicit Tags' } + @{ Number = '2.24'; Name = 'Global Tags' } + @{ Number = '2.25'; Name = 'Unordered Set' } + @{ Number = '2.26'; Name = 'Ordered Mapping' } + @{ Number = '2.27'; Name = 'Invoice' } + @{ Number = '2.28'; Name = 'Log File' } + ) { + $yaml = Get-Content -Path (Join-Path $chapterPath "$Number.yaml") -Raw + + ($yaml | Test-Yaml) | Should -BeTrue + { $yaml | ConvertFrom-Yaml -AsHashtable -NoEnumerate } | Should -Not -Throw + } + + It 'constructs the block and flow collection examples' { + $sequence = Get-Content -Path (Join-Path $chapterPath '2.01.yaml') -Raw | + ConvertFrom-Yaml -NoEnumerate + $mapping = Get-Content -Path (Join-Path $chapterPath '2.06.yaml') -Raw | + ConvertFrom-Yaml + + $sequence | Should -Be @('Mark McGwire', 'Sammy Sosa', 'Ken Griffey') + $mapping.'Mark McGwire'.hr | Should -Be 65 + $mapping.'Sammy Sosa'.avg | Should -Be 0.288 + } + + It 'constructs multi-document streams from Examples 2.7, 2.8, and 2.28' { + $ranking = @( + Get-Content -Path (Join-Path $chapterPath '2.07.yaml') -Raw | + ConvertFrom-Yaml -NoEnumerate + ) + $feed = @( + Get-Content -Path (Join-Path $chapterPath '2.08.yaml') -Raw | + ConvertFrom-Yaml + ) + $log = @( + Get-Content -Path (Join-Path $chapterPath '2.28.yaml') -Raw | + ConvertFrom-Yaml + ) + + $ranking.Count | Should -Be 2 + $feed.Count | Should -Be 2 + $feed[1].action | Should -Be 'grand slam' + $log.Count | Should -Be 3 + $log[2].Stack[1].code | Should -Be 'foo = bar' + } + + It 'constructs literal, folded, quoted, and multi-line scalars' { + $literal = Get-Content -Path (Join-Path $chapterPath '2.13.yaml') -Raw | + ConvertFrom-Yaml + $folded = Get-Content -Path (Join-Path $chapterPath '2.14.yaml') -Raw | + ConvertFrom-Yaml + $quoted = Get-Content -Path (Join-Path $chapterPath '2.17.yaml') -Raw | + ConvertFrom-Yaml + $multiLine = Get-Content -Path (Join-Path $chapterPath '2.18.yaml') -Raw | + ConvertFrom-Yaml + + $literal | Should -Match '\\//\|\|' + $folded | Should -Be "Mark McGwire's year was crippled by a knee injury.`n" + $quoted.unicode | Should -Be "Sosa did fine.$([char]0x263A)" + $quoted.quoted | Should -Be " # Not a 'comment'." + $multiLine.plain | Should -Be 'This unquoted scalar spans many lines.' + } + + It 'constructs the Chapter 2 numeric examples with the core schema' { + $integers = Get-Content -Path (Join-Path $chapterPath '2.19.yaml') -Raw | + ConvertFrom-Yaml + $floats = Get-Content -Path (Join-Path $chapterPath '2.20.yaml') -Raw | + ConvertFrom-Yaml + + $integers.canonical | Should -Be 12345 + $integers.octal | Should -Be 12 + $integers.hexadecimal | Should -Be 12 + $floats.fixed | Should -Be 1230.15 + [double]::IsNegativeInfinity($floats.'negative infinity') | Should -BeTrue + [double]::IsNaN($floats.'not a number') | Should -BeTrue + } + + It 'keeps untagged timestamps as strings in Example 2.22' { + $timestamps = Get-Content -Path (Join-Path $chapterPath '2.22.yaml') -Raw | + ConvertFrom-Yaml + + $timestamps.canonical | Should -BeOfType [string] + $timestamps.iso8601 | Should -BeOfType [string] + $timestamps.spaced | Should -BeOfType [string] + $timestamps.date | Should -BeOfType [string] + } + + It 'handles explicit and application tags safely in Examples 2.23 and 2.24' { + $tagged = Get-Content -Path (Join-Path $chapterPath '2.23.yaml') -Raw | + ConvertFrom-Yaml + $shapes = Get-Content -Path (Join-Path $chapterPath '2.24.yaml') -Raw | + ConvertFrom-Yaml -NoEnumerate + + $tagged.'not-date' | Should -Be '2002-04-28' + , $tagged.picture | Should -BeOfType [byte[]] + $tagged.'application specific tag' | Should -BeOfType [string] + [object]::ReferenceEquals($shapes[0].center, $shapes[1].start) | Should -BeTrue + [object]::ReferenceEquals($shapes[0].center, $shapes[2].start) | Should -BeTrue + } + + It 'constructs set and ordered-map tags from Examples 2.25 and 2.26' { + $set = Get-Content -Path (Join-Path $chapterPath '2.25.yaml') -Raw | + ConvertFrom-Yaml + $orderedMap = Get-Content -Path (Join-Path $chapterPath '2.26.yaml') -Raw | + ConvertFrom-Yaml + + $set | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + @($set.Keys) | Should -Be @('Mark McGwire', 'Sammy Sosa', 'Ken Griffey') + $orderedMap | Should -BeOfType [System.Collections.Specialized.OrderedDictionary] + @($orderedMap.Keys) | Should -Be @('Mark McGwire', 'Sammy Sosa', 'Ken Griffey') + } + + It 'preserves the invoice address alias identity in Example 2.27' { + $invoice = Get-Content -Path (Join-Path $chapterPath '2.27.yaml') -Raw | + ConvertFrom-Yaml + + [object]::ReferenceEquals($invoice.'bill-to', $invoice.'ship-to') | Should -BeTrue + $invoice.product.Count | Should -Be 2 + $invoice.total | Should -Be 4443.52 + } +} + +Describe 'Pinned yaml-test-suite reference cases' { + BeforeAll { + $suitePath = Join-Path $PSScriptRoot 'fixtures\yaml-test-suite' + } + + It 'accepts valid reference case ' -ForEach @( + @{ Case = 'M5DY' } + @{ Case = 'SBG9' } + @{ Case = '6BFJ' } + @{ Case = '565N' } + @{ Case = 'EHF6' } + @{ Case = 'VJP3-valid' } + ) { + $yaml = Get-Content -Path (Join-Path $suitePath "$Case.yaml") -Raw + + ($yaml | Test-Yaml) | Should -BeTrue + { $yaml | ConvertFrom-Yaml -AsHashtable -NoEnumerate } | Should -Not -Throw + } + + It 'rejects invalid reference case ' -ForEach @( + @{ Case = '2JQS' } + @{ Case = 'SF5V' } + @{ Case = 'H7TQ' } + @{ Case = 'VJP3-invalid' } + ) { + $yaml = Get-Content -Path (Join-Path $suitePath "$Case.yaml") -Raw + + ($yaml | Test-Yaml) | Should -BeFalse + } + + It 'constructs both binary spellings in case 565N identically' { + $yaml = Get-Content -Path (Join-Path $suitePath '565N.yaml') -Raw + $result = $yaml | ConvertFrom-Yaml + + [System.Linq.Enumerable]::SequenceEqual[byte]( + $result.canonical, + $result.generic + ) | Should -BeTrue + } +} diff --git a/tests/Test-Yaml.Tests.ps1 b/tests/Test-Yaml.Tests.ps1 new file mode 100644 index 0000000..74a80b4 --- /dev/null +++ b/tests/Test-Yaml.Tests.ps1 @@ -0,0 +1,86 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +BeforeAll { + . (Join-Path $PSScriptRoot 'TestBootstrap.ps1') +} + +Describe 'Test-Yaml' { + It 'returns true for valid YAML and an empty stream' { + ('name: Ada' | Test-Yaml) | Should -BeTrue + ('' | Test-Yaml) | Should -BeTrue + } + + It 'joins pipeline lines consistently with ConvertFrom-Yaml' { + ('name: Ada', 'items: [one, two]' | Test-Yaml) | Should -BeTrue + } + + It 'returns false for malformed syntax' { + ('items: [one, two' | Test-Yaml) | Should -BeFalse + } + + It 'returns false for duplicate mapping keys' { + ("key: one`nkey: two" | Test-Yaml) | Should -BeFalse + ("1: one`n01: two" | Test-Yaml) | Should -BeFalse + } + + It 'accepts complex keys even though default object projection cannot' { + ("? [a, b]`n: value" | Test-Yaml) | Should -BeTrue + } + + It 'returns false when a configured safety limit is exceeded' { + ("a:`n b:`n c: value" | Test-Yaml -Depth 2) | Should -BeFalse + ("[one, two]" | Test-Yaml -MaxNodes 2) | Should -BeFalse + ("a: &a value`nb: *a" | Test-Yaml -MaxAliases 0) | Should -BeFalse + ('value: long' | Test-Yaml -MaxScalarLength 4) | Should -BeFalse + } + + It 'uses fixed-size fingerprints for an alias DAG' { + $lines = [System.Collections.Generic.List[string]]::new() + $lines.Add('base: &a0 [x, x]') + for ($level = 1; $level -le 12; $level++) { + $lines.Add(('level{0}: &a{0} [*a{1}, *a{1}]' -f $level, ($level - 1))) + } + $lines.Add('? *a12') + $lines.Add(': value') + $yaml = $lines -join "`n" + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $result = Test-Yaml -Yaml $yaml -MaxNodes 100 -MaxAliases 100 + $stopwatch.Stop() + + $document = @(Read-YamlStreamCore -Yaml $yaml -Depth 100 -MaxNodes 100 -MaxAliases 100 ` + -MaxScalarLength 1048576)[0] + $cache = [System.Collections.Generic.Dictionary[int, string]]::new() + $hasher = [System.Security.Cryptography.SHA256]::Create() + try { + Test-YamlNodeGraph -Node $document -Visited ([System.Collections.Generic.HashSet[int]]::new()) ` + -FingerprintCache $cache -FingerprintHasher $hasher + } finally { + $hasher.Dispose() + } + + $result | Should -BeTrue + $stopwatch.Elapsed.TotalSeconds | Should -BeLessThan 5 + @($cache.Values | Where-Object Length -NE 44).Count | Should -Be 0 + } + + It 'does not swallow an unexpected runtime failure' { + Mock Read-YamlStream { + throw [System.InvalidOperationException]::new('unexpected runtime failure') + } + + { 'name: Ada' | Test-Yaml } | + Should -Throw -ExpectedMessage '*unexpected runtime failure*' + } +} diff --git a/tests/TestBootstrap.ps1 b/tests/TestBootstrap.ps1 new file mode 100644 index 0000000..7c7900f --- /dev/null +++ b/tests/TestBootstrap.ps1 @@ -0,0 +1,10 @@ +$assemblyPath = Join-Path $PSScriptRoot '..\src\assemblies\YamlDotNet.dll' +[void][System.Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath)) + +Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\private') -Filter '*.ps1' | + Sort-Object Name | + ForEach-Object { . $_.FullName } + +Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\public') -Filter '*.ps1' | + Sort-Object Name | + ForEach-Object { . $_.FullName } diff --git a/tests/fixtures/yaml-spec-1.2.2/SOURCES.txt b/tests/fixtures/yaml-spec-1.2.2/SOURCES.txt new file mode 100644 index 0000000..04c5707 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/SOURCES.txt @@ -0,0 +1,18 @@ +YAML 1.2.2 Chapter 2 examples +================================ + +Source repository: + https://github.com/yaml/yaml-spec +Pinned commit: + 1b1a1be43bd6e0cfec45caf0e40af3b5d2bb7f8a +Source file: + spec/1.2.2/spec.md +Published specification: + https://yaml.org/spec/1.2.2/ + +The files in chapter-02 are exact YAML snippets from Examples 2.1 through +2.28. They are named by example number. + +The specification states: + + This document may be freely copied, provided it is not modified. diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.01.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.01.yaml new file mode 100644 index 0000000..d12e671 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.01.yaml @@ -0,0 +1,3 @@ +- Mark McGwire +- Sammy Sosa +- Ken Griffey diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.02.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.02.yaml new file mode 100644 index 0000000..7b7ec94 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.02.yaml @@ -0,0 +1,3 @@ +hr: 65 # Home runs +avg: 0.278 # Batting average +rbi: 147 # Runs Batted In diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.03.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.03.yaml new file mode 100644 index 0000000..01883b9 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.03.yaml @@ -0,0 +1,8 @@ +american: +- Boston Red Sox +- Detroit Tigers +- New York Yankees +national: +- New York Mets +- Chicago Cubs +- Atlanta Braves diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.04.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.04.yaml new file mode 100644 index 0000000..430f6b3 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.04.yaml @@ -0,0 +1,8 @@ +- + name: Mark McGwire + hr: 65 + avg: 0.278 +- + name: Sammy Sosa + hr: 63 + avg: 0.288 diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.05.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.05.yaml new file mode 100644 index 0000000..cdd7770 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.05.yaml @@ -0,0 +1,3 @@ +- [name , hr, avg ] +- [Mark McGwire, 65, 0.278] +- [Sammy Sosa , 63, 0.288] diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.06.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.06.yaml new file mode 100644 index 0000000..3d7bf39 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.06.yaml @@ -0,0 +1,5 @@ +Mark McGwire: {hr: 65, avg: 0.278} +Sammy Sosa: { + hr: 63, + avg: 0.288, + } diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.07.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.07.yaml new file mode 100644 index 0000000..bc711d5 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.07.yaml @@ -0,0 +1,10 @@ +# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.08.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.08.yaml new file mode 100644 index 0000000..05e102d --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.08.yaml @@ -0,0 +1,10 @@ +--- +time: 20:03:20 +player: Sammy Sosa +action: strike (miss) +... +--- +time: 20:03:47 +player: Sammy Sosa +action: grand slam +... diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.09.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.09.yaml new file mode 100644 index 0000000..ab74579 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.09.yaml @@ -0,0 +1,8 @@ +--- +hr: # 1998 hr ranking +- Mark McGwire +- Sammy Sosa +# 1998 rbi ranking +rbi: +- Sammy Sosa +- Ken Griffey diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.10.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.10.yaml new file mode 100644 index 0000000..d2484be --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.10.yaml @@ -0,0 +1,8 @@ +--- +hr: +- Mark McGwire +# Following node labeled SS +- &SS Sammy Sosa +rbi: +- *SS # Subsequent occurrence +- Ken Griffey diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.11.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.11.yaml new file mode 100644 index 0000000..e12ac8b --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.11.yaml @@ -0,0 +1,8 @@ +? - Detroit Tigers + - Chicago cubs +: - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.12.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.12.yaml new file mode 100644 index 0000000..63a8ed7 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.12.yaml @@ -0,0 +1,8 @@ +--- +# Products purchased +- item : Super Hoop + quantity: 1 +- item : Basketball + quantity: 4 +- item : Big Shoes + quantity: 1 diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.13.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.13.yaml new file mode 100644 index 0000000..13fb656 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.13.yaml @@ -0,0 +1,4 @@ +# ASCII Art +--- | + \//||\/|| + // || ||__ diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.14.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.14.yaml new file mode 100644 index 0000000..fb4ed4a --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.14.yaml @@ -0,0 +1,4 @@ +--- > + Mark McGwire's + year was crippled + by a knee injury. diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.15.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.15.yaml new file mode 100644 index 0000000..09cbf06 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.15.yaml @@ -0,0 +1,8 @@ +--- > + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.16.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.16.yaml new file mode 100644 index 0000000..9f66d88 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.16.yaml @@ -0,0 +1,7 @@ +name: Mark McGwire +accomplishment: > + Mark set a major league + home run record in 1998. +stats: | + 65 Home Runs + 0.278 Batting Average diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.17.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.17.yaml new file mode 100644 index 0000000..c5c2a18 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.17.yaml @@ -0,0 +1,7 @@ +unicode: "Sosa did fine.\u263A" +control: "\b1998\t1999\t2000\n" +hex esc: "\x0d\x0a is \r\n" + +single: '"Howdy!" he cried.' +quoted: ' # Not a ''comment''.' +tie-fighter: '|\-*-/|' diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.18.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.18.yaml new file mode 100644 index 0000000..e0a8bfa --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.18.yaml @@ -0,0 +1,6 @@ +plain: + This unquoted scalar + spans many lines. + +quoted: "So does this + quoted scalar.\n" diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.19.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.19.yaml new file mode 100644 index 0000000..830ab3f --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.19.yaml @@ -0,0 +1,4 @@ +canonical: 12345 +decimal: +12345 +octal: 0o14 +hexadecimal: 0xC diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.20.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.20.yaml new file mode 100644 index 0000000..074c729 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.20.yaml @@ -0,0 +1,5 @@ +canonical: 1.23015e+3 +exponential: 12.3015e+02 +fixed: 1230.15 +negative infinity: -.inf +not a number: .nan diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.21.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.21.yaml new file mode 100644 index 0000000..510165d --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.21.yaml @@ -0,0 +1,3 @@ +null: +booleans: [ true, false ] +string: '012345' diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.22.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.22.yaml new file mode 100644 index 0000000..aaac185 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.22.yaml @@ -0,0 +1,4 @@ +canonical: 2001-12-15T02:59:43.1Z +iso8601: 2001-12-14t21:59:43.10-05:00 +spaced: 2001-12-14 21:59:43.10 -5 +date: 2002-12-14 diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.23.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.23.yaml new file mode 100644 index 0000000..5dbd992 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.23.yaml @@ -0,0 +1,13 @@ +--- +not-date: !!str 2002-04-28 + +picture: !!binary | + R0lGODlhDAAMAIQAAP//9/X + 17unp5WZmZgAAAOfn515eXv + Pz7Y6OjuDg4J+fn5OTk6enp + 56enmleECcgggoBADs= + +application specific tag: !something | + The semantics of the tag + above may be different for + different documents. diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.24.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.24.yaml new file mode 100644 index 0000000..1180757 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.24.yaml @@ -0,0 +1,14 @@ +%TAG ! tag:clarkevans.com,2002: +--- !shape + # Use the ! handle for presenting + # tag:clarkevans.com,2002:circle +- !circle + center: &ORIGIN {x: 73, y: 129} + radius: 7 +- !line + start: *ORIGIN + finish: { x: 89, y: 102 } +- !label + start: *ORIGIN + color: 0xFFEEBB + text: Pretty vector drawing. diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.25.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.25.yaml new file mode 100644 index 0000000..a723d5d --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.25.yaml @@ -0,0 +1,7 @@ +# Sets are represented as a +# Mapping where each key is +# associated with a null value +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griffey diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.26.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.26.yaml new file mode 100644 index 0000000..5d871bf --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.26.yaml @@ -0,0 +1,7 @@ +# Ordered maps are represented as +# A sequence of mappings, with +# each mapping having one key +--- !!omap +- Mark McGwire: 65 +- Sammy Sosa: 63 +- Ken Griffey: 58 diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.27.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.27.yaml new file mode 100644 index 0000000..6a82497 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.27.yaml @@ -0,0 +1,29 @@ +--- ! +invoice: 34843 +date : 2001-01-23 +bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +ship-to: *id001 +product: +- sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 +- sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. diff --git a/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.28.yaml b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.28.yaml new file mode 100644 index 0000000..44c6325 --- /dev/null +++ b/tests/fixtures/yaml-spec-1.2.2/chapter-02/2.28.yaml @@ -0,0 +1,26 @@ +--- +Time: 2001-11-23 15:01:42 -5 +User: ed +Warning: + This is an error message + for the log file +--- +Time: 2001-11-23 15:02:31 -5 +User: ed +Warning: + A slightly different error + message. +--- +Date: 2001-11-23 15:03:17 -5 +User: ed +Fatal: + Unknown variable "bar" +Stack: +- file: TopClass.py + line: 23 + code: | + x = MoreObject("345\n") +- file: MoreClass.py + line: 58 + code: |- + foo = bar diff --git a/tests/fixtures/yaml-test-suite/2JQS.yaml b/tests/fixtures/yaml-test-suite/2JQS.yaml new file mode 100644 index 0000000..d0a086d --- /dev/null +++ b/tests/fixtures/yaml-test-suite/2JQS.yaml @@ -0,0 +1,2 @@ +: a +: b diff --git a/tests/fixtures/yaml-test-suite/565N.yaml b/tests/fixtures/yaml-test-suite/565N.yaml new file mode 100644 index 0000000..dcdb16f --- /dev/null +++ b/tests/fixtures/yaml-test-suite/565N.yaml @@ -0,0 +1,12 @@ +canonical: !!binary "\ + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\ + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\ + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=" +generic: !!binary | + R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5 + OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+ + +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC + AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs= +description: + The binary value above is a tiny arrow encoded as a gif image. diff --git a/tests/fixtures/yaml-test-suite/6BFJ.yaml b/tests/fixtures/yaml-test-suite/6BFJ.yaml new file mode 100644 index 0000000..19d1e3e --- /dev/null +++ b/tests/fixtures/yaml-test-suite/6BFJ.yaml @@ -0,0 +1,3 @@ +--- +&mapping +&key [ &item a, b, c ]: value diff --git a/tests/fixtures/yaml-test-suite/EHF6.yaml b/tests/fixtures/yaml-test-suite/EHF6.yaml new file mode 100644 index 0000000..f69ccde --- /dev/null +++ b/tests/fixtures/yaml-test-suite/EHF6.yaml @@ -0,0 +1,4 @@ +!!map { + k: !!seq + [ a, !!str b] +} diff --git a/tests/fixtures/yaml-test-suite/H7TQ.yaml b/tests/fixtures/yaml-test-suite/H7TQ.yaml new file mode 100644 index 0000000..79aadcd --- /dev/null +++ b/tests/fixtures/yaml-test-suite/H7TQ.yaml @@ -0,0 +1,2 @@ +%YAML 1.2 foo +--- diff --git a/tests/fixtures/yaml-test-suite/LICENSE.txt b/tests/fixtures/yaml-test-suite/LICENSE.txt new file mode 100644 index 0000000..5059e95 --- /dev/null +++ b/tests/fixtures/yaml-test-suite/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2020 Ingy döt Net + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/yaml-test-suite/M5DY.yaml b/tests/fixtures/yaml-test-suite/M5DY.yaml new file mode 100644 index 0000000..9123ce2 --- /dev/null +++ b/tests/fixtures/yaml-test-suite/M5DY.yaml @@ -0,0 +1,9 @@ +? - Detroit Tigers + - Chicago cubs +: + - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] diff --git a/tests/fixtures/yaml-test-suite/SBG9.yaml b/tests/fixtures/yaml-test-suite/SBG9.yaml new file mode 100644 index 0000000..e6e30e2 --- /dev/null +++ b/tests/fixtures/yaml-test-suite/SBG9.yaml @@ -0,0 +1 @@ +{a: [b, c], [d, e]: f} diff --git a/tests/fixtures/yaml-test-suite/SF5V.yaml b/tests/fixtures/yaml-test-suite/SF5V.yaml new file mode 100644 index 0000000..cb5488f --- /dev/null +++ b/tests/fixtures/yaml-test-suite/SF5V.yaml @@ -0,0 +1,3 @@ +%YAML 1.2 +%YAML 1.2 +--- diff --git a/tests/fixtures/yaml-test-suite/SOURCES.txt b/tests/fixtures/yaml-test-suite/SOURCES.txt new file mode 100644 index 0000000..e94dab1 --- /dev/null +++ b/tests/fixtures/yaml-test-suite/SOURCES.txt @@ -0,0 +1,22 @@ +yaml-test-suite reference fixtures +================================== + +Source repository: + https://github.com/yaml/yaml-test-suite +Pinned commit: + da267a5c4782e7361e82889e76c0dc7df0e1e870 +Source directory: + src/ + +The local YAML payloads are copied from these pinned test cases: + 2JQS Duplicate empty mapping keys + M5DY Complex sequence keys + SBG9 Flow sequence as a mapping key + 6BFJ Anchors on a mapping and complex key + 565N Explicit binary construction + EHF6 Explicit map and sequence tags + SF5V Duplicate YAML directive (invalid) + H7TQ Extra directive words (invalid) + VJP3 Multi-line flow indentation, invalid and valid variants + +See LICENSE.txt for the upstream MIT license. diff --git a/tests/fixtures/yaml-test-suite/VJP3-invalid.yaml b/tests/fixtures/yaml-test-suite/VJP3-invalid.yaml new file mode 100644 index 0000000..79c6eda --- /dev/null +++ b/tests/fixtures/yaml-test-suite/VJP3-invalid.yaml @@ -0,0 +1,5 @@ +k: { +k +: +v +} diff --git a/tests/fixtures/yaml-test-suite/VJP3-valid.yaml b/tests/fixtures/yaml-test-suite/VJP3-valid.yaml new file mode 100644 index 0000000..1d71c2a --- /dev/null +++ b/tests/fixtures/yaml-test-suite/VJP3-valid.yaml @@ -0,0 +1,5 @@ +k: { + k + : + v + } From 7715d3e8e4493130fad507c04482d9589bfa2b87 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:54:14 +0200 Subject: [PATCH 4/8] feat: add safe YAML serialization Normalize supported PowerShell data, emit a YAML 1.2-compatible subset, preserve repeated acyclic references, reject cycles and normalized duplicate keys, and cover packaging metadata. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../private/Confirm-YamlScalarLength.ps1 | 20 ++ src/functions/private/ConvertTo-YamlNode.ps1 | 296 ++++++++++++++++++ src/functions/private/ConvertTo-YamlText.ps1 | 51 +++ .../Get-YamlEmissionNodeFingerprint.ps1 | 79 +++++ .../private/New-YamlEmissionNode.ps1 | 30 ++ .../New-YamlSerializationException.ps1 | 30 ++ src/functions/private/Set-YamlNodeAnchor.ps1 | 23 ++ src/functions/private/Write-YamlNodeEvent.ps1 | 89 ++++++ src/functions/public/ConvertTo-Yaml.ps1 | 127 ++++++++ tests/ConvertTo-Yaml.Tests.ps1 | 268 ++++++++++++++++ tests/Packaging.Tests.ps1 | 70 +++++ 11 files changed, 1083 insertions(+) create mode 100644 src/functions/private/Confirm-YamlScalarLength.ps1 create mode 100644 src/functions/private/ConvertTo-YamlNode.ps1 create mode 100644 src/functions/private/ConvertTo-YamlText.ps1 create mode 100644 src/functions/private/Get-YamlEmissionNodeFingerprint.ps1 create mode 100644 src/functions/private/New-YamlEmissionNode.ps1 create mode 100644 src/functions/private/New-YamlSerializationException.ps1 create mode 100644 src/functions/private/Set-YamlNodeAnchor.ps1 create mode 100644 src/functions/private/Write-YamlNodeEvent.ps1 create mode 100644 src/functions/public/ConvertTo-Yaml.ps1 create mode 100644 tests/ConvertTo-Yaml.Tests.ps1 create mode 100644 tests/Packaging.Tests.ps1 diff --git a/src/functions/private/Confirm-YamlScalarLength.ps1 b/src/functions/private/Confirm-YamlScalarLength.ps1 new file mode 100644 index 0000000..dfaee73 --- /dev/null +++ b/src/functions/private/Confirm-YamlScalarLength.ps1 @@ -0,0 +1,20 @@ +function Confirm-YamlScalarLength { + <# + .SYNOPSIS + Enforces the configured length limit for an emission scalar. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [pscustomobject] $State + ) + + if ($Node.Value.Length -gt $State.MaxScalarLength) { + throw (New-YamlSerializationException -ErrorId 'YamlScalarLimitExceeded' -Message ( + "A scalar exceeds the configured limit of $($State.MaxScalarLength) characters." + )) + } +} diff --git a/src/functions/private/ConvertTo-YamlNode.ps1 b/src/functions/private/ConvertTo-YamlNode.ps1 new file mode 100644 index 0000000..dd87c17 --- /dev/null +++ b/src/functions/private/ConvertTo-YamlNode.ps1 @@ -0,0 +1,296 @@ +function ConvertTo-YamlNode { + <# + .SYNOPSIS + Normalizes a supported PowerShell value to an emission node graph. + #> + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory)] + [AllowNull()] + [object] $Value, + + [Parameter(Mandatory)] + [pscustomobject] $State, + + [Parameter(Mandatory)] + [ValidateRange(1, 1024)] + [int] $Depth, + + [switch] $EnumsAsStrings + ) + + if ($Depth -gt $State.MaxDepth) { + throw (New-YamlSerializationException -ErrorId 'YamlDepthExceeded' -Message ( + "The object graph exceeds the configured depth limit of $($State.MaxDepth)." + )) + } + + $State.NodeCount++ + if ($State.NodeCount -gt $State.MaxNodes) { + throw (New-YamlSerializationException -ErrorId 'YamlNodeLimitExceeded' -Message ( + "The object graph exceeds the configured limit of $($State.MaxNodes) nodes." + )) + } + + $scalar = New-YamlEmissionNode -Kind Scalar + if ($null -eq $Value -or $Value -is [System.DBNull]) { + $scalar.Value = 'null' + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + $dataProperties = @( + $Value.PSObject.Properties | + Where-Object { + $_.IsInstance -and + $_.MemberType -eq [System.Management.Automation.PSMemberTypes]::NoteProperty + } + ) + $unsupportedProperties = @( + $Value.PSObject.Properties | + Where-Object { + $_.IsInstance -and $_.MemberType -in @( + [System.Management.Automation.PSMemberTypes]::AliasProperty, + [System.Management.Automation.PSMemberTypes]::CodeProperty, + [System.Management.Automation.PSMemberTypes]::ScriptProperty + ) + } + ) + $isPropertyBag = $Value -is [System.Management.Automation.PSCustomObject] + $isPropertyBag = $isPropertyBag -or $dataProperties.Count -gt 0 + $isPropertyBag = $isPropertyBag -or $unsupportedProperties.Count -gt 0 + + if ($unsupportedProperties.Count -gt 0) { + throw (New-YamlSerializationException -Kind NotSupported -ErrorId 'YamlUnsupportedProperty' -Message ( + "Property '$($unsupportedProperties[0].Name)' is not a note property." + )) + } + + if (-not $isPropertyBag -and ($Value -is [string] -or $Value -is [char])) { + $scalar.Value = [string] $Value + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::DoubleQuoted + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value -is [bool]) { + $scalar.Value = $Value.ToString().ToLowerInvariant() + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value.GetType().IsEnum) { + if ($EnumsAsStrings) { + $scalar.Value = $Value.ToString() + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::DoubleQuoted + } else { + $underlyingType = [System.Enum]::GetUnderlyingType($Value.GetType()) + $numericValue = [System.Convert]::ChangeType( + $Value, + $underlyingType, + [System.Globalization.CultureInfo]::InvariantCulture + ) + $scalar.Value = ([System.IConvertible] $numericValue).ToString( + [System.Globalization.CultureInfo]::InvariantCulture + ) + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + } + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + $integerTypes = @( + [System.Byte], + [System.SByte], + [System.Int16], + [System.UInt16], + [System.Int32], + [System.UInt32], + [System.Int64], + [System.UInt64], + [System.Numerics.BigInteger] + ) + if (-not $isPropertyBag -and $Value.GetType() -in $integerTypes) { + if ($Value -is [System.Numerics.BigInteger]) { + $scalar.Value = $Value.ToString([System.Globalization.CultureInfo]::InvariantCulture) + } else { + $scalar.Value = ([System.IConvertible] $Value).ToString( + [System.Globalization.CultureInfo]::InvariantCulture + ) + } + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value -is [decimal]) { + $scalar.Value = $Value.ToString([System.Globalization.CultureInfo]::InvariantCulture) + if ($scalar.Value -notmatch '[\.eE]') { + $scalar.Value += '.0' + } + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and ($Value -is [double] -or $Value -is [single])) { + $number = [double] $Value + if ([double]::IsNaN($number)) { + $scalar.Value = '.nan' + } elseif ([double]::IsPositiveInfinity($number)) { + $scalar.Value = '.inf' + } elseif ([double]::IsNegativeInfinity($number)) { + $scalar.Value = '-.inf' + } else { + $scalar.Value = $number.ToString('R', [System.Globalization.CultureInfo]::InvariantCulture) + if ($scalar.Value -notmatch '[\.eE]') { + $scalar.Value += '.0' + } + } + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::Plain + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value -is [datetimeoffset]) { + $scalar.Tag = 'tag:yaml.org,2002:timestamp' + $scalar.Value = $Value.ToString('o', [System.Globalization.CultureInfo]::InvariantCulture) + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::DoubleQuoted + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value -is [datetime]) { + $scalar.Tag = 'tag:yaml.org,2002:timestamp' + if ($Value.Kind -eq [System.DateTimeKind]::Utc) { + $scalar.Value = $Value.ToUniversalTime().ToString( + "yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", + [System.Globalization.CultureInfo]::InvariantCulture + ) + } elseif ($Value.Kind -eq [System.DateTimeKind]::Local) { + $scalar.Value = ([datetimeoffset] $Value).ToString( + 'o', + [System.Globalization.CultureInfo]::InvariantCulture + ) + } else { + $scalar.Value = $Value.ToString( + "yyyy-MM-dd'T'HH:mm:ss.fffffff", + [System.Globalization.CultureInfo]::InvariantCulture + ) + } + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::DoubleQuoted + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + if (-not $isPropertyBag -and $Value -is [byte[]]) { + $scalar.Tag = 'tag:yaml.org,2002:binary' + $scalar.Value = [System.Convert]::ToBase64String($Value) + $scalar.Style = [YamlDotNet.Core.ScalarStyle]::DoubleQuoted + Confirm-YamlScalarLength -Node $scalar -State $State + Write-Output -InputObject $scalar -NoEnumerate + return + } + + $isDictionary = $Value -is [System.Collections.IDictionary] + $isCustomObject = $isPropertyBag + $isSequence = $Value -is [System.Collections.IEnumerable] + if (-not $isDictionary -and -not $isCustomObject -and -not $isSequence) { + throw (New-YamlSerializationException -Kind NotSupported -ErrorId 'YamlUnsupportedType' -Message ( + "Values of type '$($Value.GetType().FullName)' are not supported for YAML serialization." + )) + } + + $firstTime = $false + $referenceId = $State.IdGenerator.GetId($Value, [ref] $firstTime) + if (-not $firstTime) { + $State.ReferenceCounts[$referenceId]++ + if ($State.Active.Contains($referenceId)) { + throw (New-YamlSerializationException -ErrorId 'YamlCycleDetected' -Message ( + "A cycle was detected while serializing type '$($Value.GetType().FullName)'." + )) + } + Write-Output -InputObject $State.NodesById[$referenceId] -NoEnumerate + return + } + + $State.ReferenceCounts[$referenceId] = 1 + $State.ReferenceOrder.Add($referenceId) + [void] $State.Active.Add($referenceId) + + try { + if ($isDictionary -or $isCustomObject) { + $node = New-YamlEmissionNode -Kind Mapping + $node.ReferenceId = $referenceId + $State.NodesById[$referenceId] = $node + $keyFingerprints = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::Ordinal + ) + + if ($isDictionary) { + foreach ($entry in $Value.GetEnumerator()) { + $keyNode = ConvertTo-YamlNode -Value ([object] $entry.Key) -State $State -Depth ($Depth + 1) ` + -EnumsAsStrings:$EnumsAsStrings + $keyFingerprint = Get-YamlEmissionNodeFingerprint -Node $keyNode -Cache $State.Fingerprints -Active ( + [System.Collections.Generic.HashSet[long]]::new() + ) -Hasher $State.FingerprintHasher + if (-not $keyFingerprints.Add($keyFingerprint)) { + throw (New-YamlSerializationException -ErrorId 'YamlDuplicateKey' -Message ( + 'Two mapping keys normalize to the same YAML value.' + )) + } + $valueNode = ConvertTo-YamlNode -Value ([object] $entry.Value) -State $State -Depth ($Depth + 1) ` + -EnumsAsStrings:$EnumsAsStrings + $node.Entries.Add([pscustomobject]@{ + Key = $keyNode + Value = $valueNode + }) + } + } else { + foreach ($property in $dataProperties) { + $keyNode = ConvertTo-YamlNode -Value ([object] $property.Name) -State $State -Depth ($Depth + 1) ` + -EnumsAsStrings:$EnumsAsStrings + $keyFingerprint = Get-YamlEmissionNodeFingerprint -Node $keyNode -Cache $State.Fingerprints -Active ( + [System.Collections.Generic.HashSet[long]]::new() + ) -Hasher $State.FingerprintHasher + if (-not $keyFingerprints.Add($keyFingerprint)) { + throw (New-YamlSerializationException -ErrorId 'YamlDuplicateKey' -Message ( + 'Two mapping keys normalize to the same YAML value.' + )) + } + $valueNode = ConvertTo-YamlNode -Value ([object] $property.Value) -State $State -Depth ($Depth + 1) ` + -EnumsAsStrings:$EnumsAsStrings + $node.Entries.Add([pscustomobject]@{ + Key = $keyNode + Value = $valueNode + }) + } + } + } else { + $node = New-YamlEmissionNode -Kind Sequence + $node.ReferenceId = $referenceId + $State.NodesById[$referenceId] = $node + foreach ($item in $Value) { + $itemNode = ConvertTo-YamlNode -Value ([object] $item) -State $State -Depth ($Depth + 1) ` + -EnumsAsStrings:$EnumsAsStrings + $node.Items.Add($itemNode) + } + } + } finally { + [void] $State.Active.Remove($referenceId) + } + + Write-Output -InputObject $node -NoEnumerate +} diff --git a/src/functions/private/ConvertTo-YamlText.ps1 b/src/functions/private/ConvertTo-YamlText.ps1 new file mode 100644 index 0000000..cf37efb --- /dev/null +++ b/src/functions/private/ConvertTo-YamlText.ps1 @@ -0,0 +1,51 @@ +function ConvertTo-YamlText { + <# + .SYNOPSIS + Emits an internal node graph as LF-normalized YAML text. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [ValidateRange(2, 9)] + [int] $Indent, + + [switch] $ExplicitDocumentStart + ) + + $writer = [System.IO.StringWriter]::new( + [System.Globalization.CultureInfo]::InvariantCulture + ) + try { + $settings = [YamlDotNet.Core.EmitterSettings]::new( + $Indent, + [int]::MaxValue, + $false, + 1024, + $false, + $false, + "`n", + $false + ) + $emitter = [YamlDotNet.Core.Emitter]::new($writer, $settings) + $emitter.Emit([YamlDotNet.Core.Events.StreamStart]::new()) + $emitter.Emit( + [YamlDotNet.Core.Events.DocumentStart]::new( + $null, + $null, + -not $ExplicitDocumentStart + ) + ) + Write-YamlNodeEvent -Emitter $emitter -Node $Node -EmittedReferences ( + [System.Collections.Generic.HashSet[long]]::new() + ) + $emitter.Emit([YamlDotNet.Core.Events.DocumentEnd]::new($true)) + $emitter.Emit([YamlDotNet.Core.Events.StreamEnd]::new()) + return $writer.ToString() + } finally { + $writer.Dispose() + } +} diff --git a/src/functions/private/Get-YamlEmissionNodeFingerprint.ps1 b/src/functions/private/Get-YamlEmissionNodeFingerprint.ps1 new file mode 100644 index 0000000..50a3655 --- /dev/null +++ b/src/functions/private/Get-YamlEmissionNodeFingerprint.ps1 @@ -0,0 +1,79 @@ +function Get-YamlEmissionNodeFingerprint { + <# + .SYNOPSIS + Creates a structural fingerprint for a normalized emission node. + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.Dictionary[long, string]] $Cache, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[long]] $Active, + + [Parameter(Mandatory)] + [System.Security.Cryptography.HashAlgorithm] $Hasher + ) + + $hasReferenceId = $Node.ReferenceId -ne 0 + $cachedFingerprint = '' + if ($hasReferenceId -and $Cache.TryGetValue($Node.ReferenceId, [ref] $cachedFingerprint)) { + return $cachedFingerprint + } + if ($hasReferenceId -and -not $Active.Add($Node.ReferenceId)) { + throw (New-YamlSerializationException -ErrorId 'YamlCycleDetected' -Message ( + 'A cycle was detected while fingerprinting a YAML mapping key.' + )) + } + + try { + if ($Node.Kind -eq 'Scalar') { + $isPlainImplicit = [string]::IsNullOrEmpty($Node.Tag) + $isPlainImplicit = $isPlainImplicit -and $Node.Style -eq [YamlDotNet.Core.ScalarStyle]::Plain + $resolved = Resolve-YamlScalar -Node ([pscustomobject]@{ + Tag = $Node.Tag + Value = $Node.Value + IsPlainImplicit = $isPlainImplicit + Start = [YamlDotNet.Core.Mark]::Empty + End = [YamlDotNet.Core.Mark]::Empty + }) + $fingerprint = Get-YamlScalarFingerprint -Value $resolved -Hasher $Hasher + } elseif ($Node.Kind -eq 'Sequence') { + $parts = [System.Collections.Generic.List[string]]::new() + foreach ($item in $Node.Items) { + $itemFingerprint = Get-YamlEmissionNodeFingerprint -Node $item -Cache $Cache -Active $Active ` + -Hasher $Hasher + $parts.Add($itemFingerprint) + } + $fingerprint = Get-YamlFingerprintHash -Value ('sequence:{0}' -f ($parts -join '|')) -Hasher $Hasher + } else { + $entries = [System.Collections.Generic.List[string]]::new() + foreach ($entry in $Node.Entries) { + $keyFingerprint = Get-YamlEmissionNodeFingerprint -Node $entry.Key -Cache $Cache -Active $Active ` + -Hasher $Hasher + $valueFingerprint = Get-YamlEmissionNodeFingerprint -Node $entry.Value -Cache $Cache -Active $Active ` + -Hasher $Hasher + $entryFingerprint = Get-YamlFingerprintHash -Value "entry:$keyFingerprint=$valueFingerprint" ` + -Hasher $Hasher + $entries.Add($entryFingerprint) + } + $entries.Sort([System.StringComparer]::Ordinal) + $fingerprint = Get-YamlFingerprintHash -Value ('mapping:{0}' -f ($entries -join '|')) -Hasher $Hasher + } + + if ($hasReferenceId) { + $Cache[$Node.ReferenceId] = $fingerprint + } + return $fingerprint + } finally { + if ($hasReferenceId) { + [void] $Active.Remove($Node.ReferenceId) + } + } +} diff --git a/src/functions/private/New-YamlEmissionNode.ps1 b/src/functions/private/New-YamlEmissionNode.ps1 new file mode 100644 index 0000000..bf155be --- /dev/null +++ b/src/functions/private/New-YamlEmissionNode.ps1 @@ -0,0 +1,30 @@ +function New-YamlEmissionNode { + <# + .SYNOPSIS + Creates an internal YAML emission node. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Constructs an in-memory representation node without changing system state.' + )] + [CmdletBinding()] + [OutputType([pscustomobject])] + param ( + [Parameter(Mandatory)] + [ValidateSet('Mapping', 'Scalar', 'Sequence')] + [string] $Kind + ) + + $node = [pscustomobject]@{ + PSTypeName = 'PSModule.Yaml.EmissionNode' + Kind = $Kind + Tag = '' + Value = '' + Style = [YamlDotNet.Core.ScalarStyle]::Any + Items = [System.Collections.Generic.List[object]]::new() + Entries = [System.Collections.Generic.List[object]]::new() + ReferenceId = [long] 0 + Anchor = '' + } + Write-Output -InputObject $node -NoEnumerate +} diff --git a/src/functions/private/New-YamlSerializationException.ps1 b/src/functions/private/New-YamlSerializationException.ps1 new file mode 100644 index 0000000..69c39c8 --- /dev/null +++ b/src/functions/private/New-YamlSerializationException.ps1 @@ -0,0 +1,30 @@ +function New-YamlSerializationException { + <# + .SYNOPSIS + Creates a typed YAML serialization exception. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Constructs an in-memory exception without changing system state.' + )] + [CmdletBinding()] + [OutputType([System.Exception])] + param ( + [Parameter(Mandatory)] + [string] $Message, + + [Parameter(Mandatory)] + [string] $ErrorId, + + [ValidateSet('InvalidOperation', 'NotSupported')] + [string] $Kind = 'InvalidOperation' + ) + + $exception = if ($Kind -eq 'NotSupported') { + [System.NotSupportedException]::new($Message) + } else { + [System.InvalidOperationException]::new($Message) + } + $exception.Data['YamlErrorId'] = $ErrorId + Write-Output -InputObject $exception -NoEnumerate +} diff --git a/src/functions/private/Set-YamlNodeAnchor.ps1 b/src/functions/private/Set-YamlNodeAnchor.ps1 new file mode 100644 index 0000000..1e562e5 --- /dev/null +++ b/src/functions/private/Set-YamlNodeAnchor.ps1 @@ -0,0 +1,23 @@ +function Set-YamlNodeAnchor { + <# + .SYNOPSIS + Assigns deterministic anchors to repeated emission nodes. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', + Justification = 'Assigns anchors within a private in-memory representation graph.' + )] + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [pscustomobject] $State + ) + + $anchorNumber = 1 + foreach ($referenceId in $State.ReferenceOrder) { + if ($State.ReferenceCounts[$referenceId] -gt 1) { + $State.NodesById[$referenceId].Anchor = 'id{0:d3}' -f $anchorNumber + $anchorNumber++ + } + } +} diff --git a/src/functions/private/Write-YamlNodeEvent.ps1 b/src/functions/private/Write-YamlNodeEvent.ps1 new file mode 100644 index 0000000..ae5a150 --- /dev/null +++ b/src/functions/private/Write-YamlNodeEvent.ps1 @@ -0,0 +1,89 @@ +function Write-YamlNodeEvent { + <# + .SYNOPSIS + Writes one normalized node graph to the low-level YAML emitter. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [YamlDotNet.Core.Emitter] $Emitter, + + [Parameter(Mandatory)] + [pscustomobject] $Node, + + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[long]] $EmittedReferences + ) + + if ($Node.ReferenceId -ne 0 -and $EmittedReferences.Contains($Node.ReferenceId)) { + $Emitter.Emit( + [YamlDotNet.Core.Events.AnchorAlias]::new( + [YamlDotNet.Core.AnchorName]::new($Node.Anchor) + ) + ) + return + } + + if ($Node.ReferenceId -ne 0) { + [void] $EmittedReferences.Add($Node.ReferenceId) + } + + $anchor = if ([string]::IsNullOrEmpty($Node.Anchor)) { + [YamlDotNet.Core.AnchorName]::Empty + } else { + [YamlDotNet.Core.AnchorName]::new($Node.Anchor) + } + $tag = if ([string]::IsNullOrEmpty($Node.Tag)) { + [YamlDotNet.Core.TagName]::Empty + } else { + [YamlDotNet.Core.TagName]::new($Node.Tag) + } + + if ($Node.Kind -eq 'Scalar') { + $isPlainImplicit = [string]::IsNullOrEmpty($Node.Tag) -and $Node.Style -eq [YamlDotNet.Core.ScalarStyle]::Plain + $isQuotedImplicit = [string]::IsNullOrEmpty($Node.Tag) -and -not $isPlainImplicit + $Emitter.Emit( + [YamlDotNet.Core.Events.Scalar]::new( + $anchor, + $tag, + $Node.Value, + $Node.Style, + $isPlainImplicit, + $isQuotedImplicit + ) + ) + return + } + + $isImplicit = [string]::IsNullOrEmpty($Node.Tag) + if ($Node.Kind -eq 'Sequence') { + $Emitter.Emit( + [YamlDotNet.Core.Events.SequenceStart]::new( + $anchor, + $tag, + $isImplicit, + [YamlDotNet.Core.Events.SequenceStyle]::Block + ) + ) + foreach ($item in $Node.Items) { + Write-YamlNodeEvent -Emitter $Emitter -Node $item -EmittedReferences $EmittedReferences + } + $Emitter.Emit([YamlDotNet.Core.Events.SequenceEnd]::new()) + return + } + + $Emitter.Emit( + [YamlDotNet.Core.Events.MappingStart]::new( + $anchor, + $tag, + $isImplicit, + [YamlDotNet.Core.Events.MappingStyle]::Block + ) + ) + foreach ($entry in $Node.Entries) { + Write-YamlNodeEvent -Emitter $Emitter -Node $entry.Key -EmittedReferences $EmittedReferences + Write-YamlNodeEvent -Emitter $Emitter -Node $entry.Value -EmittedReferences $EmittedReferences + } + $Emitter.Emit([YamlDotNet.Core.Events.MappingEnd]::new()) +} diff --git a/src/functions/public/ConvertTo-Yaml.ps1 b/src/functions/public/ConvertTo-Yaml.ps1 new file mode 100644 index 0000000..9baafbc --- /dev/null +++ b/src/functions/public/ConvertTo-Yaml.ps1 @@ -0,0 +1,127 @@ +function ConvertTo-Yaml { + <# + .SYNOPSIS + Converts PowerShell values to YAML 1.2-compatible text. + + .DESCRIPTION + Normalizes supported PowerShell values into mappings, sequences, and + scalars before emitting YAML through YamlDotNet's low-level emitter. + PowerShell metadata is not serialized. Repeated acyclic collection + references use YAML anchors and aliases; cyclic and unsupported values + terminate with a specific error. + + Multiple pipeline records are collected into one top-level sequence. + + .PARAMETER InputObject + A value to serialize. Multiple pipeline records become one sequence. + + .PARAMETER Depth + Maximum object-graph nesting depth. The default is 100. + + .PARAMETER MaxNodes + Maximum number of traversed nodes. The default is 100000. + + .PARAMETER MaxScalarLength + Maximum character count for one emitted scalar. The default is + 1048576. + + .PARAMETER Indent + Block indentation from 2 through 9 spaces. The default is 2. + + .PARAMETER ExplicitDocumentStart + Emits an explicit `---` document start marker. + + .PARAMETER EnumsAsStrings + Emits enum names as strings instead of underlying numeric values. + + .EXAMPLE + [ordered]@{ name = 'Ada'; active = $true } | ConvertTo-Yaml + + Converts one mapping to YAML. + + .EXAMPLE + 'one', 'two' | ConvertTo-Yaml + + Converts two pipeline records to one YAML sequence. + + .INPUTS + System.Object + + .OUTPUTS + System.String + #> + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowNull()] + [object] $InputObject, + + [ValidateRange(1, 1024)] + [int] $Depth = 100, + + [ValidateRange(1, 2147483647)] + [int] $MaxNodes = 100000, + + [ValidateRange(1, 2147483647)] + [int] $MaxScalarLength = 1048576, + + [ValidateRange(2, 9)] + [int] $Indent = 2, + + [switch] $ExplicitDocumentStart, + + [switch] $EnumsAsStrings + ) + + begin { + $values = [System.Collections.Generic.List[object]]::new() + } + process { + $values.Add($InputObject) + } + end { + if ($values.Count -eq 0) { + if (-not $PSBoundParameters.ContainsKey('InputObject')) { + return + } + $value = [object[]]::new(0) + } elseif ($values.Count -eq 1) { + $value = $values[0] + } else { + $value = [object[]] $values.ToArray() + } + $state = [pscustomobject]@{ + IdGenerator = [System.Runtime.Serialization.ObjectIDGenerator]::new() + NodesById = [System.Collections.Generic.Dictionary[long, object]]::new() + ReferenceCounts = [System.Collections.Generic.Dictionary[long, int]]::new() + ReferenceOrder = [System.Collections.Generic.List[long]]::new() + Fingerprints = [System.Collections.Generic.Dictionary[long, string]]::new() + FingerprintHasher = [System.Security.Cryptography.SHA256]::Create() + Active = [System.Collections.Generic.HashSet[long]]::new() + NodeCount = 0 + MaxDepth = $Depth + MaxNodes = $MaxNodes + MaxScalarLength = $MaxScalarLength + } + + try { + $node = ConvertTo-YamlNode -Value ([object] $value) -State $state -Depth 1 ` + -EnumsAsStrings:$EnumsAsStrings + Set-YamlNodeAnchor -State $state + $yaml = ConvertTo-YamlText -Node $node -Indent $Indent ` + -ExplicitDocumentStart:$ExplicitDocumentStart + $PSCmdlet.WriteObject($yaml, $false) + } catch [System.NotSupportedException] { + $record = New-YamlErrorRecord -Exception $_.Exception -DefaultErrorId 'YamlUnsupportedType' ` + -Category InvalidType -TargetObject $value + $PSCmdlet.ThrowTerminatingError($record) + } catch [System.InvalidOperationException] { + $record = New-YamlErrorRecord -Exception $_.Exception -DefaultErrorId 'YamlSerializationFailed' ` + -Category InvalidOperation -TargetObject $value + $PSCmdlet.ThrowTerminatingError($record) + } finally { + $state.FingerprintHasher.Dispose() + } + } +} diff --git a/tests/ConvertTo-Yaml.Tests.ps1 b/tests/ConvertTo-Yaml.Tests.ps1 new file mode 100644 index 0000000..caf573d --- /dev/null +++ b/tests/ConvertTo-Yaml.Tests.ps1 @@ -0,0 +1,268 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +BeforeAll { + . (Join-Path $PSScriptRoot 'TestBootstrap.ps1') +} + +Describe 'ConvertTo-Yaml' { + Context 'Supported values' { + It 'serializes mappings, sequences, and core scalars to valid YAML' { + $inputObject = [ordered]@{ + name = 'Ada' + active = $true + count = 42 + ratio = 1.5 + nothing = $null + items = @('one', 'two') + } + + $yaml = ConvertTo-Yaml -InputObject $inputObject + $result = $yaml | ConvertFrom-Yaml -AsHashtable + + $yaml | Should -BeOfType [string] + ($yaml | Test-Yaml) | Should -BeTrue + $result['name'] | Should -Be 'Ada' + $result['active'] | Should -BeTrue + $result['count'] | Should -Be 42 + $result['ratio'] | Should -Be 1.5 + $result['nothing'] | Should -BeNullOrEmpty + $result['items'] | Should -Be @('one', 'two') + } + + It 'quotes strings that resemble core-schema values' { + $inputObject = [ordered]@{ + boolean = 'true' + integer = '42' + null = 'null' + empty = '' + } + + $result = ($inputObject | ConvertTo-Yaml) | ConvertFrom-Yaml -AsHashtable + + $result['boolean'] | Should -BeOfType [string] + $result['integer'] | Should -BeOfType [string] + $result['null'] | Should -BeOfType [string] + $result['empty'] | Should -BeOfType [string] + } + + It 'serializes signed, unsigned, large, decimal, and special numbers' { + $inputObject = [ordered]@{ + signed = [long] -9223372036854775808 + unsigned = [ulong]::MaxValue + big = [System.Numerics.BigInteger]::Parse('18446744073709551616') + decimal = [decimal] 12.50 + positive = [double]::PositiveInfinity + negative = [double]::NegativeInfinity + nan = [double]::NaN + } + + $result = ($inputObject | ConvertTo-Yaml) | ConvertFrom-Yaml -AsHashtable + + $result['signed'] | Should -Be ([long] -9223372036854775808) + $result['unsigned'] | Should -BeOfType [System.Numerics.BigInteger] + $result['big'] | Should -BeOfType [System.Numerics.BigInteger] + $result['decimal'] | Should -Be 12.5 + [double]::IsPositiveInfinity($result['positive']) | Should -BeTrue + [double]::IsNegativeInfinity($result['negative']) | Should -BeTrue + [double]::IsNaN($result['nan']) | Should -BeTrue + } + + It 'serializes DateTime, DateTimeOffset, and binary values with standard tags' { + $inputObject = [ordered]@{ + utc = [datetime]::new(2026, 7, 19, 13, 49, 21, [DateTimeKind]::Utc) + local = [datetimeoffset]::Parse('2026-07-19T15:49:21+02:00') + binary = [Text.Encoding]::UTF8.GetBytes('hello') + } + + $yaml = $inputObject | ConvertTo-Yaml + $result = $yaml | ConvertFrom-Yaml -AsHashtable + + $yaml | Should -Match '!!timestamp' + $yaml | Should -Match '!!binary' + $result['utc'] | Should -BeOfType [datetimeoffset] + $result['local'] | Should -BeOfType [datetimeoffset] + [Text.Encoding]::UTF8.GetString($result['binary']) | Should -Be 'hello' + } + + It 'serializes enum values numerically or by name' { + $numeric = ([ordered]@{ day = [DayOfWeek]::Monday }) | ConvertTo-Yaml + $named = ([ordered]@{ day = [DayOfWeek]::Monday }) | + ConvertTo-Yaml -EnumsAsStrings + + ($numeric | ConvertFrom-Yaml).day | Should -Be 1 + ($named | ConvertFrom-Yaml).day | Should -Be 'Monday' + ($named | ConvertFrom-Yaml).day | Should -BeOfType [string] + } + + It 'serializes empty mappings and sequences distinctly' { + (ConvertTo-Yaml -InputObject @()).Trim() | Should -Be '[]' + (([ordered]@{} | ConvertTo-Yaml).Trim()) | Should -Be '{}' + + $nested = ConvertTo-Yaml -InputObject (, @()) + $nested.Trim() | Should -Be '- []' + } + + It 'serializes a null input explicitly' { + (ConvertTo-Yaml -InputObject $null).Trim() | Should -Be 'null' + } + + It 'preserves complex dictionary keys through a hashtable round trip' { + $dictionary = [System.Collections.Specialized.OrderedDictionary]::new() + $key = [object[]]@('a', 'b') + $dictionary.Add($key, 'value') + + $result = ($dictionary | ConvertTo-Yaml) | ConvertFrom-Yaml -AsHashtable + $enumerator = $result.GetEnumerator() + $null = $enumerator.MoveNext() + $roundTripKey = $enumerator.Key + + , $roundTripKey | Should -BeOfType [object[]] + $roundTripKey | Should -Be @('a', 'b') + $enumerator.Value | Should -Be 'value' + } + + It 'rejects dictionary keys that normalize to the same YAML value' { + $dictionary = [System.Collections.Specialized.OrderedDictionary]::new() + $dictionary.Add([int] 1, 'int') + $dictionary.Add([long] 1, 'long') + + { $dictionary | ConvertTo-Yaml } | + Should -Throw -ExpectedMessage '*normalize to the same YAML value*' + } + + It 'compares unordered complex keys with ordinal case-sensitive sorting' { + $firstKey = [System.Collections.Specialized.OrderedDictionary]::new() + $firstKey.Add('a', 1) + $firstKey.Add('A', 1) + $secondKey = [System.Collections.Specialized.OrderedDictionary]::new() + $secondKey.Add('A', 1) + $secondKey.Add('a', 1) + $dictionary = [System.Collections.Specialized.OrderedDictionary]::new() + $dictionary.Add($firstKey, 'first') + $dictionary.Add($secondKey, 'second') + + { $dictionary | ConvertTo-Yaml } | + Should -Throw -ExpectedMessage '*normalize to the same YAML value*' + } + } + + Context 'Pipeline and formatting' { + It 'collects multiple pipeline records into one sequence' { + $yaml = 'one', 'two', 'three' | ConvertTo-Yaml + $result = $yaml | ConvertFrom-Yaml -NoEnumerate + + $result | Should -Be @('one', 'two', 'three') + } + + It 'uses the requested indentation and document start marker' { + $yaml = [ordered]@{ + outer = [ordered]@{ + inner = 'value' + } + } | ConvertTo-Yaml -Indent 4 -ExplicitDocumentStart + + $yaml | Should -Match '^---' + $yaml | Should -Match '(?m)^ "inner":' + } + + It 'normalizes output line endings to LF' { + $yaml = [ordered]@{ one = 1; two = 2 } | ConvertTo-Yaml + + $yaml | Should -Not -Match "`r" + } + } + + Context 'References, metadata, and failures' { + It 'emits aliases for repeated acyclic references' { + $shared = [ordered]@{ value = 1 } + $inputObject = [ordered]@{ + first = $shared + second = $shared + } + + $yaml = $inputObject | ConvertTo-Yaml + $result = $yaml | ConvertFrom-Yaml + + $yaml | Should -Match '&id001' + $yaml | Should -Match '\*id001' + [object]::ReferenceEquals($result.first, $result.second) | Should -BeTrue + } + + It 'does not leak PowerShell type metadata' { + $inputObject = [pscustomobject]@{ name = 'Ada' } + $inputObject.PSObject.TypeNames.Insert(0, 'Secret.Application.Type') + + $yaml = $inputObject | ConvertTo-Yaml + + $yaml | Should -Not -Match 'Secret\.Application\.Type' + $yaml | Should -Not -Match 'PSTypeNames' + ($yaml | ConvertFrom-Yaml).name | Should -Be 'Ada' + } + + It 'serializes explicit PSObject note properties without adapted metadata' { + $inputObject = [object]::new() + $inputObject | Add-Member -MemberType NoteProperty -Name name -Value 'Ada' + + $yaml = $inputObject | ConvertTo-Yaml + $result = $yaml | ConvertFrom-Yaml + + $result.name | Should -Be 'Ada' + $yaml | Should -Not -Match 'GetType' + $yaml | Should -Not -Match 'ToString' + } + + It 'rejects cyclic structures specifically' { + $cycle = [System.Collections.ArrayList]::new() + $cycle.Add($cycle) + + { ConvertTo-Yaml -InputObject $cycle } | + Should -Throw -ExpectedMessage '*cycle was detected*' + } + + It 'rejects unsupported runtime objects instead of stringifying them' { + { ConvertTo-Yaml -InputObject ([uri] 'https://example.com') } | + Should -Throw -ExpectedMessage "*System.Uri*not supported*" + } + + It 'rejects non-data PSCustomObject properties' { + $inputObject = [pscustomobject]@{ name = 'Ada' } + $inputObject | Add-Member -MemberType ScriptProperty -Name Computed -Value { 'value' } + + { $inputObject | ConvertTo-Yaml } | + Should -Throw -ExpectedMessage "*Computed*not a note property*" + } + + It 'enforces depth, node, and scalar limits without truncating' { + $nested = [ordered]@{ a = [ordered]@{ b = [ordered]@{ c = 1 } } } + + { $nested | ConvertTo-Yaml -Depth 2 } | Should -Throw + { @(1, 2) | ConvertTo-Yaml -MaxNodes 2 } | Should -Throw + { 'long' | ConvertTo-Yaml -MaxScalarLength 3 } | Should -Throw + } + + It 'enforces the scalar limit for every emitted scalar kind' { + $bigInteger = [System.Numerics.BigInteger]::Parse('12345') + + { ConvertTo-Yaml -InputObject $null -MaxScalarLength 3 } | Should -Throw + { ConvertTo-Yaml -InputObject $true -MaxScalarLength 3 } | Should -Throw + { ConvertTo-Yaml -InputObject $bigInteger -MaxScalarLength 4 } | Should -Throw + { ConvertTo-Yaml -InputObject ([decimal] 12.5) -MaxScalarLength 3 } | Should -Throw + { ConvertTo-Yaml -InputObject ([double]::PositiveInfinity) -MaxScalarLength 3 } | Should -Throw + { ConvertTo-Yaml -InputObject ([datetime]::UtcNow) -MaxScalarLength 10 } | Should -Throw + { ConvertTo-Yaml -InputObject ([byte[]] @(1, 2, 3)) -MaxScalarLength 3 } | Should -Throw + { ConvertTo-Yaml -InputObject ([DayOfWeek]::Monday) -EnumsAsStrings -MaxScalarLength 5 } | + Should -Throw + } + } +} diff --git a/tests/Packaging.Tests.ps1 b/tests/Packaging.Tests.ps1 new file mode 100644 index 0000000..4ada169 --- /dev/null +++ b/tests/Packaging.Tests.ps1 @@ -0,0 +1,70 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.0.0'; MaximumVersion = '6.*' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Packaged dependency' { + BeforeAll { + $repositoryRoot = Split-Path -Parent $PSScriptRoot + } + + It 'bundles the exact YamlDotNet 18.1.0 netstandard2.0 assembly' { + $hash = Get-FileHash -Path ( + Join-Path $repositoryRoot 'src\assemblies\YamlDotNet.dll' + ) -Algorithm SHA256 + + $hash.Hash | Should -Be '91CD6D1FD0AE5B64BF6B252EB3B1AF2382CBC599C29045427C45FE8403D4295D' + } + + It 'keeps RequiredAssemblies out of the source manifest' { + $manifest = Import-PowerShellDataFile -Path ( + Join-Path $repositoryRoot 'src\manifest.psd1' + ) + + $manifest.DotNetFrameworkVersion | Should -Be '4.7.2' + $manifest.ContainsKey('RequiredAssemblies') | Should -BeFalse + } + + It 'packages the upstream license and dependency provenance' { + $license = Get-Content -Path ( + Join-Path $repositoryRoot 'src\licenses\YamlDotNet.LICENSE.txt' + ) -Raw + $notice = Get-Content -Path ( + Join-Path $repositoryRoot 'src\THIRD-PARTY-NOTICES.txt' + ) -Raw + + $license | Should -Match 'Copyright \(c\) 2008, 2009, 2010' + $license | Should -Match 'Permission is hereby granted, free of charge' + $notice | Should -Match 'YamlDotNet 18\.1\.0' + $notice | Should -Match 'lib/netstandard2\.0/YamlDotNet\.dll' + $notice | Should -Match '59FFE65ADE67AD9D886267F877B634A450363CB81B94E19DB9CA4C36461416F6' + $notice | Should -Match '91CD6D1FD0AE5B64BF6B252EB3B1AF2382CBC599C29045427C45FE8403D4295D' + } + + It 'uses Process-PSModule 6.1.4 and treats tests as important changes' { + $workflow = Get-Content -Path ( + Join-Path $repositoryRoot '.github\workflows\Process-PSModule.yml' + ) -Raw + + $workflow | Should -Match 'workflow\.yml@da180bac16b13bfbcdf08b2e4e221b5b49e5ff28 # v6\.1\.4' + $workflow | Should -Match '\^src/' + $workflow | Should -Match '\^tests/' + } + + It 'does not add a custom assembly loader to module source' { + $source = Get-ChildItem -Path (Join-Path $repositoryRoot 'src') -Recurse -File | + Where-Object Extension -EQ '.ps1' | + Get-Content -Raw + + $source | Should -Not -Match '\bAdd-Type\b' + $source | Should -Not -Match 'Assembly\]::Load' + } +} From 4e472cf9bb90bf46bc140bedb8aac357166ed8b6 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 16:54:22 +0200 Subject: [PATCH 5/8] docs: describe YAML compatibility boundaries Document command contracts, safety limits, pipeline behavior, supported data types, unknown-tag handling, and non-preserved YAML presentation details with working examples. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 175 ++++++++++++++++++++++++++++++++++--------- examples/General.ps1 | 52 +++++++++---- 2 files changed, 177 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 96936ee..465af3f 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,170 @@ -# {{ NAME }} +# Yaml -{{ DESCRIPTION }} +`Yaml` converts between YAML streams and PowerShell values. It uses +YamlDotNet's low-level parser and emitter while applying YAML 1.2 core-schema +rules in PowerShell, without activating .NET types from YAML tags. -## Prerequisites +## Installation + +```powershell +Install-PSResource -Name Yaml +Import-Module -Name Yaml +``` -This uses the following external resources: -- The [PSModule framework](https://github.com/PSModule) for building, testing and publishing the module. +The module exports: -## Installation +| Command | Purpose | +| --- | --- | +| `ConvertFrom-Yaml` | Parse one or more YAML documents into PowerShell values. | +| `ConvertTo-Yaml` | Serialize supported PowerShell values as YAML 1.2-compatible text. | +| `Test-Yaml` | Test YAML syntax, tags, duplicate keys, and configured resource limits. | + +## Parse YAML -To install the module from the PowerShell Gallery, you can use the following command: +Ordinary string-key mappings become ordered `PSCustomObject` values. A +top-level sequence writes its items to the pipeline by default. ```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} +$config = @' +name: example +enabled: true +ports: [80, 443] +'@ | ConvertFrom-Yaml + +$config.name +$config.ports[0] ``` -## Usage +Pipeline strings are joined with LF and parsed as one stream. This makes +line-oriented input work as expected: -Here is a list of example that are typical use cases for the module. - -### Example 1: Greet an entity +```powershell +$config = Get-Content -Path '.\config.yaml' | ConvertFrom-Yaml +``` -Provide examples for typical commands that a user would like to do with the module. +Use `-NoEnumerate` when a top-level sequence must remain one pipeline record: ```powershell -Greet-Entity -Name 'World' -Hello, World! +$servers = @' +- name: web-1 +- name: web-2 +'@ | ConvertFrom-Yaml -NoEnumerate ``` -### Example 2 +Every YAML document is returned separately: + +```powershell +$documents = @(@' +--- +name: first +--- +name: second +'@ | ConvertFrom-Yaml) +``` -Provide examples for typical commands that a user would like to do with the module. +Use `-AsHashtable` for insertion-ordered dictionaries and mappings with +complex, non-string, empty, or case-colliding keys: ```powershell -Import-Module -Name PSModuleTemplate +$mapping = @' +? [region, port] +: eu-1 +'@ | ConvertFrom-Yaml -AsHashtable ``` -### Find more examples +## Serialize PowerShell values -To find more examples of how to use the module, please refer to the [examples](examples) folder. +`ConvertTo-Yaml` supports `PSCustomObject` and explicit PSObject note-property +bags, dictionaries, sequences, strings, characters, Booleans, integer and +floating-point numbers, `BigInteger`, `DateTime`, `DateTimeOffset`, enums, +null, and byte arrays. -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. +```powershell +$yaml = [ordered]@{ + name = 'example' + enabled = $true + ports = @(80, 443) +} | ConvertTo-Yaml -ExplicitDocumentStart -## Documentation +$roundTrip = $yaml | ConvertFrom-Yaml +``` -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +Multiple pipeline records are collected into one top-level YAML sequence: -## Contributing +```powershell +'one', 'two' | ConvertTo-Yaml +``` + +Pass an array directly when it represents one input value: -Coder or not, you can contribute to the project! We welcome all contributions. +```powershell +$items = @('one', 'two') +ConvertTo-Yaml -InputObject $items +``` + +`-EnumsAsStrings` emits enum names instead of their underlying numeric values. +`-Indent` accepts 2 through 9 spaces. `-Depth`, `-MaxNodes`, and +`-MaxScalarLength` constrain serialization. -### For Users +Repeated acyclic collection references are emitted with anchors and aliases. +Cyclic graphs and unsupported runtime objects fail specifically; values are +never silently truncated or converted with `ToString()`. -If you don't code, you still sit on valuable information that can make this project even better. If you experience that the -product does unexpected things, throw errors or is missing functionality, you can help by submitting bugs and feature requests. -Please see the issues tab on this project and submit a new issue that matches your needs. +## Validate YAML -### For Developers +```powershell +if (Get-Content -Path '.\config.yaml' | Test-Yaml) { + 'The YAML stream is valid.' +} +``` -If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. -You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. +`Test-Yaml` uses the same parser and limits as `ConvertFrom-Yaml`. It returns +`$false` for YAML-specific failures, including duplicate keys and resource +limit violations. Unexpected runtime failures are not suppressed. + +## Data model and safety + +- Plain implicit scalars use the YAML 1.2 core schema. YAML 1.1-only Boolean + words such as `yes` and untagged timestamps remain strings. +- Standard explicit tags are handled without reflection: `str`, `null`, + `bool`, `int`, `float`, `binary`, `timestamp`, `seq`, `map`, `set`, `omap`, + and `pairs`. +- Unknown application tags are discarded safely. Tagged scalars become + strings and tagged collections retain their sequence or mapping shape. +- Duplicate mapping keys are rejected after scalar construction, including + structurally equal complex keys. +- Aliases preserve collection reference identity where PowerShell can + represent it. +- YAML merge keys are not expanded; `<<` is ordinary mapping data under the + YAML 1.2 core schema. +- Parsing defaults to depth 100, 100000 nodes, 1000 aliases, and 1048576 + decoded characters per scalar. The corresponding limit parameters can be + lowered for untrusted input. + +Default object projection requires mapping keys that can be represented +without loss as PowerShell properties. Use `-AsHashtable` when that restriction +does not fit the data. + +## Compatibility boundaries + +The module preserves data values and graph sharing where representable. A +PowerShell object does not retain YAML presentation details, so data round +trips do **not** preserve comments, scalar style, tag spelling or handles, +anchor names, mapping presentation, line endings, or source formatting. +Unknown application tags are not reconstructed. + +Exact numeric CLR widths and enum CLR types are also not reconstructed after a +YAML round trip. The emitter writes a deliberately limited YAML +1.2-compatible subset even though YamlDotNet describes its emitter as YAML +1.1. + +## Third-party component + +The packaged module includes YamlDotNet 18.1.0 +`lib/netstandard2.0/YamlDotNet.dll`. See +[`src/THIRD-PARTY-NOTICES.txt`](src/THIRD-PARTY-NOTICES.txt) and +[`src/licenses/YamlDotNet.LICENSE.txt`](src/licenses/YamlDotNet.LICENSE.txt). -## Acknowledgements +## Contributing -Here is a list of people and projects that helped this project in some way. +See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/examples/General.ps1 b/examples/General.ps1 index e193423..2c428b4 100644 --- a/examples/General.ps1 +++ b/examples/General.ps1 @@ -1,19 +1,45 @@ -<# - .SYNOPSIS - This is a general example of how to use the module. +<# + .SYNOPSIS + Demonstrates the public Yaml commands. #> -# Import the module -Import-Module -Name 'PSModule' +Import-Module -Name Yaml -# Define the path to the font file -$FontFilePath = 'C:\Fonts\CodeNewRoman\CodeNewRomanNerdFontPropo-Regular.tff' +$yaml = @' +--- +name: example +enabled: true +ports: [80, 443] +'@ -# Install the font -Install-Font -Path $FontFilePath -Verbose +# Parse a mapping to an ordered PSCustomObject. +$config = $yaml | ConvertFrom-Yaml +$config -# List installed fonts -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' +# Keep a top-level sequence as one pipeline record. +$servers = @' +- name: web-1 +- name: web-2 +'@ | ConvertFrom-Yaml -NoEnumerate +$servers.Count -# Uninstall the font -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' | Uninstall-Font -Verbose +# Preserve mappings whose keys cannot be PowerShell property names. +$complexMapping = @' +? [region, port] +: eu-1 +'@ | ConvertFrom-Yaml -AsHashtable +$complexMapping + +# Serialize supported PowerShell data and parse it again. +$outputYaml = [ordered]@{ + name = 'example' + enabled = $true + ports = @(80, 443) +} | ConvertTo-Yaml -ExplicitDocumentStart + +$outputYaml +$outputYaml | ConvertFrom-Yaml + +# Test syntax, duplicate keys, tags, and resource limits without conversion. +$isValid = $outputYaml | Test-Yaml +$isValid From 9bba8e0022e66d9e29c494dc704d7bd8e7a89c81 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 17:01:07 +0200 Subject: [PATCH 6/8] ci: exercise the built module in tests Prefer the workflow-imported artifact so Pester coverage measures real module execution, exclude pinned YAML fixtures from repository config linting, and skip PlatyPS docs generation because it preloads a conflicting YamlDotNet assembly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/PSModule.yml | 8 ++++--- tests/Test-Yaml.Tests.ps1 | 24 +++++++++----------- tests/TestBootstrap.ps1 | 48 ++++++++++++++++++++++++++++++++------- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/.github/PSModule.yml b/.github/PSModule.yml index b37f9f1..5f3e463 100644 --- a/.github/PSModule.yml +++ b/.github/PSModule.yml @@ -16,12 +16,14 @@ Test: # Skip: true # MacOS: # Skip: true -# Build: -# Docs: -# Skip: true +Build: + Docs: + # PlatyPS loads a conflicting YamlDotNet assembly before importing this module. + Skip: true Linter: env: + FILTER_REGEX_EXCLUDE: '.*tests/fixtures/.*' VALIDATE_BIOME_FORMAT: false VALIDATE_BIOME_LINT: false VALIDATE_GITHUB_ACTIONS_ZIZMOR: false diff --git a/tests/Test-Yaml.Tests.ps1 b/tests/Test-Yaml.Tests.ps1 index 74a80b4..159b937 100644 --- a/tests/Test-Yaml.Tests.ps1 +++ b/tests/Test-Yaml.Tests.ps1 @@ -59,25 +59,23 @@ Describe 'Test-Yaml' { $result = Test-Yaml -Yaml $yaml -MaxNodes 100 -MaxAliases 100 $stopwatch.Stop() - $document = @(Read-YamlStreamCore -Yaml $yaml -Depth 100 -MaxNodes 100 -MaxAliases 100 ` - -MaxScalarLength 1048576)[0] - $cache = [System.Collections.Generic.Dictionary[int, string]]::new() - $hasher = [System.Security.Cryptography.SHA256]::Create() - try { - Test-YamlNodeGraph -Node $document -Visited ([System.Collections.Generic.HashSet[int]]::new()) ` - -FingerprintCache $cache -FingerprintHasher $hasher - } finally { - $hasher.Dispose() - } + $fingerprintLengths = @(Get-TestYamlFingerprintLength -Yaml $yaml) $result | Should -BeTrue $stopwatch.Elapsed.TotalSeconds | Should -BeLessThan 5 - @($cache.Values | Where-Object Length -NE 44).Count | Should -Be 0 + @($fingerprintLengths | Where-Object { $_ -ne 44 }).Count | Should -Be 0 } It 'does not swallow an unexpected runtime failure' { - Mock Read-YamlStream { - throw [System.InvalidOperationException]::new('unexpected runtime failure') + $loadedModule = Get-Module -Name Yaml | Select-Object -First 1 + if ($null -eq $loadedModule) { + Mock Read-YamlStream { + throw [System.InvalidOperationException]::new('unexpected runtime failure') + } + } else { + Mock Read-YamlStream -ModuleName $loadedModule.Name { + throw [System.InvalidOperationException]::new('unexpected runtime failure') + } } { 'name: Ada' | Test-Yaml } | diff --git a/tests/TestBootstrap.ps1 b/tests/TestBootstrap.ps1 index 7c7900f..742b752 100644 --- a/tests/TestBootstrap.ps1 +++ b/tests/TestBootstrap.ps1 @@ -1,10 +1,42 @@ -$assemblyPath = Join-Path $PSScriptRoot '..\src\assemblies\YamlDotNet.dll' -[void][System.Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath)) +$yamlModule = Get-Module -Name Yaml | Select-Object -First 1 +if ($null -eq $yamlModule) { + $assemblyPath = Join-Path $PSScriptRoot '..\src\assemblies\YamlDotNet.dll' + [void][System.Reflection.Assembly]::LoadFrom((Resolve-Path $assemblyPath)) -Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\private') -Filter '*.ps1' | - Sort-Object Name | - ForEach-Object { . $_.FullName } + Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\private') -Filter '*.ps1' | + Sort-Object Name | + ForEach-Object { . $_.FullName } -Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\public') -Filter '*.ps1' | - Sort-Object Name | - ForEach-Object { . $_.FullName } + Get-ChildItem -Path (Join-Path $PSScriptRoot '..\src\functions\public') -Filter '*.ps1' | + Sort-Object Name | + ForEach-Object { . $_.FullName } +} + +function Get-TestYamlFingerprintLength { + param ( + [Parameter(Mandatory)] + [string] $Yaml + ) + + $implementation = { + param ([string] $YamlText) + + $document = @(Read-YamlStreamCore -Yaml $YamlText -Depth 100 -MaxNodes 100 -MaxAliases 100 ` + -MaxScalarLength 1048576)[0] + $cache = [System.Collections.Generic.Dictionary[int, string]]::new() + $hasher = [System.Security.Cryptography.SHA256]::Create() + try { + Test-YamlNodeGraph -Node $document -Visited ([System.Collections.Generic.HashSet[int]]::new()) ` + -FingerprintCache $cache -FingerprintHasher $hasher + } finally { + $hasher.Dispose() + } + @($cache.Values | ForEach-Object Length) + } + + $loadedModule = Get-Module -Name Yaml | Select-Object -First 1 + if ($null -eq $loadedModule) { + return @(& $implementation $Yaml) + } + return @(& $loadedModule $implementation $Yaml) +} From df8913ca652ce864b74704dc8b8669ecc7fd11e8 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 17:03:54 +0200 Subject: [PATCH 7/8] ci: exclude YAML conformance fixtures Use yamllint's native ignore rules for vendored spec and invalid parser fixtures without relying on unsupported reusable-workflow environment settings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/PSModule.yml | 1 - .github/linters/.yaml-lint.yml | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .github/linters/.yaml-lint.yml diff --git a/.github/PSModule.yml b/.github/PSModule.yml index 5f3e463..aa919aa 100644 --- a/.github/PSModule.yml +++ b/.github/PSModule.yml @@ -23,7 +23,6 @@ Build: Linter: env: - FILTER_REGEX_EXCLUDE: '.*tests/fixtures/.*' VALIDATE_BIOME_FORMAT: false VALIDATE_BIOME_LINT: false VALIDATE_GITHUB_ACTIONS_ZIZMOR: false diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml new file mode 100644 index 0000000..03f7d32 --- /dev/null +++ b/.github/linters/.yaml-lint.yml @@ -0,0 +1,6 @@ +--- +extends: relaxed + +# Official conformance fixtures intentionally preserve invalid and unusual YAML. +ignore: | + tests/fixtures/ From a04a69e7b1a60795f1f18a5e68fe7972a89868a7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Jul 2026 17:08:43 +0200 Subject: [PATCH 8/8] ci: match fixture paths in Super-Linter Match absolute fixture paths passed by Super-Linter and satisfy repository analysis for the coverage test helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/linters/.yaml-lint.yml | 2 +- tests/TestBootstrap.ps1 | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml index 03f7d32..12ac3bb 100644 --- a/.github/linters/.yaml-lint.yml +++ b/.github/linters/.yaml-lint.yml @@ -3,4 +3,4 @@ extends: relaxed # Official conformance fixtures intentionally preserve invalid and unusual YAML. ignore: | - tests/fixtures/ + **/tests/fixtures/** diff --git a/tests/TestBootstrap.ps1 b/tests/TestBootstrap.ps1 index 742b752..a39e83e 100644 --- a/tests/TestBootstrap.ps1 +++ b/tests/TestBootstrap.ps1 @@ -13,6 +13,10 @@ if ($null -eq $yamlModule) { } function Get-TestYamlFingerprintLength { + <# + .SYNOPSIS + Returns structural fingerprint lengths for a YAML alias graph. + #> param ( [Parameter(Mandatory)] [string] $Yaml