Skip to content

Commit 707fc2a

Browse files
fix: decode tag URI escapes
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b4bc395 commit 707fc2a

7 files changed

Lines changed: 133 additions & 14 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
function ConvertFrom-YamlTagUriText {
2+
<#
3+
.SYNOPSIS
4+
Decodes percent-encoded UTF-8 octets in YAML tag URI text.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([string])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[AllowEmptyString()]
11+
[string] $Text
12+
)
13+
14+
$builder = [System.Text.StringBuilder]::new($Text.Length)
15+
$bytes = [System.Collections.Generic.List[byte]]::new()
16+
$utf8 = [System.Text.UTF8Encoding]::new($false, $true)
17+
$index = 0
18+
19+
while ($index -lt $Text.Length) {
20+
if ($Text[$index] -ne '%') {
21+
[void] $builder.Append($Text[$index])
22+
$index++
23+
continue
24+
}
25+
26+
$bytes.Clear()
27+
while ($index -lt $Text.Length -and $Text[$index] -eq '%') {
28+
if ($index + 2 -ge $Text.Length -or
29+
$Text[$index + 1] -notmatch '^[0-9A-Fa-f]$' -or
30+
$Text[$index + 2] -notmatch '^[0-9A-Fa-f]$') {
31+
throw [System.FormatException]::new(
32+
"The tag URI contains an incomplete or malformed percent escape at index $index."
33+
)
34+
}
35+
36+
$bytes.Add([System.Convert]::ToByte($Text.Substring($index + 1, 2), 16))
37+
$index += 3
38+
}
39+
40+
[void] $builder.Append($utf8.GetString($bytes.ToArray()))
41+
}
42+
43+
$builder.ToString()
44+
}

src/functions/private/Resolve-YamlTag.ps1

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,19 @@ function Resolve-YamlTag {
6767
}
6868
}
6969

70-
$expandedLength = $prefix.Length + $suffix.Length
70+
try {
71+
$expanded = ConvertFrom-YamlTagUriText -Text ($prefix + $suffix)
72+
} catch [System.FormatException] {
73+
throw (New-YamlException -Start $Mark -End $Mark -ErrorId 'YamlInvalidTag' -Message (
74+
"The tag token '$Token' contains a malformed URI escape."
75+
))
76+
} catch [System.Text.DecoderFallbackException] {
77+
throw (New-YamlException -Start $Mark -End $Mark -ErrorId 'YamlInvalidTag' -Message (
78+
"The tag token '$Token' contains URI escapes that are not valid UTF-8."
79+
))
80+
}
81+
82+
$expandedLength = $expanded.Length
7183
if ($expandedLength -gt $Context.MaxTagLength) {
7284
throw (New-YamlException -Start $Mark -End $Mark -ErrorId 'YamlTagLimitExceeded' -Message (
7385
"A YAML tag exceeds the configured limit of $($Context.MaxTagLength) characters."
@@ -80,7 +92,6 @@ function Resolve-YamlTag {
8092
))
8193
}
8294

83-
$expanded = $prefix + $suffix
8495
$known = $expanded -cin @(
8596
'tag:yaml.org,2002:binary',
8697
'tag:yaml.org,2002:bool',

tests/Conformance.Tests.ps1

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,12 @@ Describe 'Released yaml-test-suite corpus accounting' {
4848
}
4949

5050
It 'accounts for parser representation/event comparisons' {
51-
@($suiteResults | Where-Object EventResult -EQ 'Pass').Count | Should -Be 307
51+
@($suiteResults | Where-Object EventResult -EQ 'Pass').Count | Should -Be 308
5252
@($suiteResults | Where-Object EventResult -EQ 'PolicyDifference').Count |
53-
Should -Be 1
53+
Should -Be 0
5454
@($suiteResults | Where-Object EventResult -EQ 'Fail').Count | Should -Be 0
5555
@($suiteResults | Where-Object EventResult -EQ 'NotApplicable').Count |
5656
Should -Be 94
57-
@(
58-
$suiteResults |
59-
Where-Object EventResult -EQ 'PolicyDifference' |
60-
Select-Object -ExpandProperty Case |
61-
Sort-Object
62-
) | Should -Be @('6CK3')
6357
}
6458

6559
It 'accounts for JSON construction comparisons' {

tests/ConvertFrom-Yaml.Tests.ps1

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,37 @@ mapping: !<tag:example.test,2026:object>
187187
$result.mapping.name | Should -Be 'safe'
188188
}
189189

190+
It 'decodes percent escapes in expanded representation tags' {
191+
$escaped = Get-TestYamlRepresentationRoot -Yaml (
192+
"%TAG !e! tag:example.com,2000:app/`n--- !e!tag%21 value"
193+
)
194+
$multibyte = Get-TestYamlRepresentationRoot -Yaml (
195+
"%TAG !e! tag:example.com,2000:app/`n--- !e!caf%C3%A9 value"
196+
)
197+
198+
$escaped.Tag | Should -Be 'tag:example.com,2000:app/tag!'
199+
$escaped.HasUnknownTag | Should -BeTrue
200+
$multibyte.Tag | Should -Be (
201+
'tag:example.com,2000:app/caf{0}' -f [char] 0x00E9
202+
)
203+
$multibyte.HasUnknownTag | Should -BeTrue
204+
}
205+
206+
It 'retains unknown local and global tags before neutral value projection' {
207+
$local = Get-TestYamlRepresentationRoot -Yaml '!local value'
208+
$global = Get-TestYamlRepresentationRoot -Yaml (
209+
'!<tag:example.test,2026:object> value'
210+
)
211+
212+
$local.Tag | Should -Be '!local'
213+
$local.HasUnknownTag | Should -BeTrue
214+
$global.Tag | Should -Be 'tag:example.test,2026:object'
215+
$global.HasUnknownTag | Should -BeTrue
216+
('!local value' | ConvertFrom-Yaml) | Should -Be 'value'
217+
('!<tag:example.test,2026:object> value' | ConvertFrom-Yaml) |
218+
Should -Be 'value'
219+
}
220+
190221
It 'preserves repeated collection references' {
191222
$result = @'
192223
source: &source

tests/Test-Yaml.Tests.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ Describe 'Test-Yaml' {
4848
("a: &a value`nb: &b *a" | Test-Yaml) | Should -BeFalse
4949
}
5050

51+
It 'rejects malformed or invalid UTF-8 tag URI escapes: <Tag>' -ForEach @(
52+
@{ Tag = '!value%' }
53+
@{ Tag = '!value%2' }
54+
@{ Tag = '!value%GG' }
55+
@{ Tag = '!value%C3' }
56+
@{ Tag = '!value%C3%28' }
57+
) {
58+
("$Tag value" | Test-Yaml) | Should -BeFalse
59+
{ "$Tag value" | ConvertFrom-Yaml } | Should -Throw
60+
}
61+
5162
It 'recognizes document markers only at column zero' {
5263
("key:`n ---" | Test-Yaml) | Should -BeTrue
5364
("key:`n ..." | Test-Yaml) | Should -BeTrue

tests/TestBootstrap.ps1

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function Get-TestYamlFingerprintLength {
5151
} finally {
5252
$hasher.Dispose()
5353
}
54+
5455
@($cache.Values | ForEach-Object Length)
5556
}
5657

@@ -60,3 +61,34 @@ function Get-TestYamlFingerprintLength {
6061
}
6162
return @(& $loadedModule $implementation $Yaml)
6263
}
64+
65+
function Get-TestYamlRepresentationRoot {
66+
<#
67+
.SYNOPSIS
68+
Returns observable metadata from the root representation node.
69+
#>
70+
param (
71+
[Parameter(Mandatory)]
72+
[string] $Yaml
73+
)
74+
75+
$implementation = {
76+
param ([string] $YamlText)
77+
78+
$document = (Read-YamlStreamCore -Yaml $YamlText -Depth 100 -MaxNodes 100 -MaxAliases 100 `
79+
-MaxScalarLength 1048576 -MaxTagLength 1024 -MaxTotalTagLength 65536 `
80+
-MaxNumericLength 4096).Value[0]
81+
[pscustomobject]@{
82+
Kind = $document.Kind
83+
Tag = $document.Tag
84+
HasUnknownTag = $document.HasUnknownTag
85+
Value = $document.Value
86+
}
87+
}
88+
89+
$loadedModule = Get-Module -Name Yaml | Select-Object -First 1
90+
if ($null -eq $loadedModule) {
91+
return & $implementation $Yaml
92+
}
93+
return & $loadedModule $implementation $Yaml
94+
}

tests/tools/Invoke-YamlTestSuite.ps1

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,6 @@ function Get-YamlSuitePolicyReason {
259259
if ($YamlText -cmatch '!!(?:binary|omap|pairs|set|timestamp)(?:[ \t\r\n,\[\]\{\}]|$)') {
260260
return 'StandardTagProjectionPolicy'
261261
}
262-
if ($YamlText -cmatch '(?:^|[ \t\r\n\[\]\{\},])!<(?!tag:yaml\.org,2002:)' -or
263-
$YamlText -cmatch '(?:^|[ \t\r\n\[\]\{\},])![^\s!<][^\s:,\]\}\{]*') {
264-
return 'UnknownTagProjectionPolicy'
265-
}
266262
if (($Expected -match 'unsupported:' -or $Actual -match 'unsupported:')) {
267263
return 'PowerShellTypePolicy'
268264
}

0 commit comments

Comments
 (0)