Skip to content

Commit 3655dcc

Browse files
Classify timestamp boundary failures
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 810adde commit 3655dcc

3 files changed

Lines changed: 104 additions & 17 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
function ConvertTo-YamlTimestampText {
2+
<#
3+
.SYNOPSIS
4+
Formats a representable CLR timestamp for YAML emission.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([string])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[object] $Value
11+
)
12+
13+
try {
14+
if ($Value -is [datetimeoffset]) {
15+
$offsetMinutes = $Value.Offset.TotalMinutes
16+
if ($offsetMinutes -lt -840 -or $offsetMinutes -gt 840 -or
17+
$offsetMinutes -ne [System.Math]::Truncate($offsetMinutes) -or
18+
$Value.UtcTicks -lt [datetime]::MinValue.Ticks -or
19+
$Value.UtcTicks -gt [datetime]::MaxValue.Ticks) {
20+
throw [System.ArgumentOutOfRangeException]::new('Value')
21+
}
22+
return $Value.ToString(
23+
'o',
24+
[System.Globalization.CultureInfo]::InvariantCulture
25+
)
26+
}
27+
28+
if ($Value.Kind -eq [System.DateTimeKind]::Local) {
29+
return [datetimeoffset]::new($Value).ToString(
30+
'o',
31+
[System.Globalization.CultureInfo]::InvariantCulture
32+
)
33+
}
34+
35+
$utc = if ($Value.Kind -eq [System.DateTimeKind]::Utc) {
36+
$Value
37+
} else {
38+
[datetime]::SpecifyKind($Value, [System.DateTimeKind]::Utc)
39+
}
40+
return $utc.ToString(
41+
"yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'",
42+
[System.Globalization.CultureInfo]::InvariantCulture
43+
)
44+
} catch [System.ArgumentException] {
45+
throw (New-YamlSerializationException -ErrorId 'YamlTimestampSerializationFailed' `
46+
-Message 'The timestamp cannot be represented with its local or explicit UTC offset.')
47+
} catch [System.FormatException] {
48+
throw (New-YamlSerializationException -ErrorId 'YamlTimestampSerializationFailed' `
49+
-Message 'The timestamp cannot be represented with its local or explicit UTC offset.')
50+
}
51+
}

src/functions/private/Get-YamlSerializationShape.ps1

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,29 +162,14 @@ function Get-YamlSerializationShape {
162162
}
163163
if (-not $isPropertyBag -and $Value -is [datetimeoffset]) {
164164
$scalar.Tag = 'tag:yaml.org,2002:timestamp'
165-
$scalar.Value = $Value.ToString('o', [System.Globalization.CultureInfo]::InvariantCulture)
165+
$scalar.Value = ConvertTo-YamlTimestampText -Value $Value
166166
$scalar.Style = 'DoubleQuoted'
167167
Confirm-YamlScalarLength -Node $scalar -State $State
168168
return [pscustomobject]@{ Kind = 'Scalar'; Node = $scalar; Values = $null }
169169
}
170170
if (-not $isPropertyBag -and $Value -is [datetime]) {
171171
$scalar.Tag = 'tag:yaml.org,2002:timestamp'
172-
if ($Value.Kind -eq [System.DateTimeKind]::Local) {
173-
$scalar.Value = ([datetimeoffset] $Value).ToString(
174-
'o',
175-
[System.Globalization.CultureInfo]::InvariantCulture
176-
)
177-
} else {
178-
$utc = if ($Value.Kind -eq [System.DateTimeKind]::Utc) {
179-
$Value
180-
} else {
181-
[datetime]::SpecifyKind($Value, [System.DateTimeKind]::Utc)
182-
}
183-
$scalar.Value = $utc.ToString(
184-
"yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'",
185-
[System.Globalization.CultureInfo]::InvariantCulture
186-
)
187-
}
172+
$scalar.Value = ConvertTo-YamlTimestampText -Value $Value
188173
$scalar.Style = 'DoubleQuoted'
189174
Confirm-YamlScalarLength -Node $scalar -State $State
190175
return [pscustomobject]@{ Kind = 'Scalar'; Node = $scalar; Values = $null }

tests/ConvertTo-Yaml.Tests.ps1

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,57 @@ Describe 'ConvertTo-Yaml' {
211211
[Text.Encoding]::UTF8.GetString($result['binary']) | Should -Be 'hello'
212212
}
213213

214+
It 'serializes every representable DateTimeOffset boundary stably' {
215+
$values = @(
216+
[datetimeoffset]::MinValue
217+
[datetimeoffset]::MaxValue
218+
[datetimeoffset]::new(
219+
[datetime]::new(1, 1, 1, 14, 0, 0),
220+
[timespan]::FromHours(14)
221+
)
222+
[datetimeoffset]::new(
223+
[datetime]::new(9999, 12, 31, 9, 59, 59, 999).AddTicks(9999),
224+
[timespan]::FromHours(-14)
225+
)
226+
)
227+
228+
foreach ($value in $values) {
229+
$yaml = ConvertTo-Yaml -InputObject $value
230+
$roundTrip = $yaml | ConvertFrom-Yaml
231+
232+
($yaml | Test-Yaml) | Should -BeTrue
233+
$roundTrip.UtcTicks | Should -Be $value.UtcTicks
234+
}
235+
}
236+
237+
It 'classifies unrepresentable local DateTime boundaries' {
238+
$errors = [System.Collections.Generic.List[object]]::new()
239+
$successes = 0
240+
foreach ($value in @(
241+
[datetime]::SpecifyKind([datetime]::MinValue, [DateTimeKind]::Local)
242+
[datetime]::SpecifyKind([datetime]::MaxValue, [DateTimeKind]::Local)
243+
)) {
244+
try {
245+
$yaml = ConvertTo-Yaml -InputObject $value
246+
($yaml | Test-Yaml) | Should -BeTrue
247+
$successes++
248+
} catch {
249+
$errors.Add($_)
250+
}
251+
}
252+
253+
($successes + $errors.Count) | Should -Be 2
254+
if ([TimeZoneInfo]::Local.BaseUtcOffset -ne [timespan]::Zero) {
255+
$errors.Count | Should -BeGreaterThan 0
256+
}
257+
foreach ($serializationError in $errors) {
258+
$serializationError.FullyQualifiedErrorId |
259+
Should -Be 'YamlTimestampSerializationFailed,ConvertTo-Yaml'
260+
$serializationError.Exception.Message |
261+
Should -Be 'The timestamp cannot be represented with its local or explicit UTC offset.'
262+
}
263+
}
264+
214265
It 'emits aliases for repeated byte arrays and preserves their identity' {
215266
$bytes = [Text.Encoding]::UTF8.GetBytes('hello')
216267
$inputObject = [ordered]@{ first = $bytes; second = $bytes }

0 commit comments

Comments
 (0)