diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs index 66313f832d..82e1277e00 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertFromJsonCommand.cs @@ -49,6 +49,12 @@ namespace Microsoft.PowerShell.Commands [Parameter] public SwitchParameter NoEnumerate { get; set; } + /// + /// Gets or sets the switch to control how DateTime values are to be parsed as a dotnet object. + /// + [Parameter] + public JsonDateKind DateKind { get; set; } = JsonDateKind.Default; + #endregion parameters #region overrides @@ -113,7 +119,7 @@ namespace Microsoft.PowerShell.Commands private bool ConvertFromJsonHelper(string input) { ErrorRecord error = null; - object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, Depth, out error); + object result = JsonObject.ConvertFromJson(input, AsHashtable.IsPresent, Depth, DateKind, out error); if (error != null) { diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonDateKind.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonDateKind.cs new file mode 100644 index 0000000000..2fed27128a --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonDateKind.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Enums for ConvertFrom-Json -DateKind parameter. + /// + public enum JsonDateKind + { + /// + /// DateTime values are returned as a DateTime with the Kind representing the time zone in the raw string. + /// + Default, + + /// + /// DateTime values are returned as the Local kind representation of the value. + /// + Local, + + /// + /// DateTime values are returned as the UTC kind representation of the value. + /// + Utc, + + /// + /// DateTime values are returned as a DateTimeOffset value preserving the timezone information. + /// + Offset, + + /// + /// DateTime values are returned as raw strings. + /// + String, + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs index f08c3a64ae..644ed5e0f7 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs @@ -151,9 +151,58 @@ namespace Microsoft.PowerShell.Commands /// if the parameter is true. [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] public static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, out ErrorRecord error) + => ConvertFromJson(input, returnHashtable, maxDepth, jsonDateKind: JsonDateKind.Default, out error); + + /// + /// Convert a JSON string back to an object of type or + /// depending on parameter . + /// + /// The JSON text to convert. + /// True if the result should be returned as a + /// instead of a . + /// The max depth allowed when deserializing the json input. Set to null for no maximum. + /// Controls how DateTime values are to be converted. + /// An error record if the conversion failed. + /// A or a + /// if the parameter is true. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")] + internal static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, JsonDateKind jsonDateKind, out ErrorRecord error) { ArgumentNullException.ThrowIfNull(input); + DateParseHandling dateParseHandling; + DateTimeZoneHandling dateTimeZoneHandling; + switch (jsonDateKind) + { + case JsonDateKind.Default: + dateParseHandling = DateParseHandling.DateTime; + dateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind; + break; + + case JsonDateKind.Local: + dateParseHandling = DateParseHandling.DateTime; + dateTimeZoneHandling = DateTimeZoneHandling.Local; + break; + + case JsonDateKind.Utc: + dateParseHandling = DateParseHandling.DateTime; + dateTimeZoneHandling = DateTimeZoneHandling.Utc; + break; + + case JsonDateKind.Offset: + dateParseHandling = DateParseHandling.DateTimeOffset; + dateTimeZoneHandling = DateTimeZoneHandling.Unspecified; + break; + + case JsonDateKind.String: + dateParseHandling = DateParseHandling.None; + dateTimeZoneHandling = DateTimeZoneHandling.Unspecified; + break; + + default: + throw new ArgumentException($"Unknown JsonDateKind value requested '{jsonDateKind}'"); + } + error = null; try { @@ -161,6 +210,9 @@ namespace Microsoft.PowerShell.Commands input, new JsonSerializerSettings { + DateParseHandling = dateParseHandling, + DateTimeZoneHandling = dateTimeZoneHandling, + // This TypeNameHandling setting is required to be secure. TypeNameHandling = TypeNameHandling.None, MetadataPropertyHandling = MetadataPropertyHandling.Ignore, diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 index f03b29e4c8..599ff35639 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 @@ -58,7 +58,7 @@ Describe 'ConvertFrom-Json Unit Tests' -tags "CI" { $json | Should -BeOfType Hashtable } } - + It 'Throws an ArgumentException with an incomplete array with AsHashtable switch set to ' -TestCase $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) { ConvertFrom-Json '["1",' -AsHashtable:$AsHashtable } | @@ -66,7 +66,7 @@ Describe 'ConvertFrom-Json Unit Tests' -tags "CI" { { ConvertFrom-Json '[' -AsHashtable:$AsHashtable } | Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand" } - + It 'Can convert multi-line object with AsHashtable switch set to ' -TestCases $testCasesWithAndWithoutAsHashtableSwitch { Param($AsHashtable) $json = @('{"a" :', '"x"}') | ConvertFrom-Json -AsHashtable:$AsHashtable @@ -161,6 +161,195 @@ b 2 c 3 "@ } + + It 'Parses DateKind Default strings for ' -TestCases @( + @{ + Value = '"2022-11-02T12:01:44.5801388+04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).LocalDateTime) + } + @{ + Value = '"2022-11-02T12:01:44.5801388-04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00"' + Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Unspecified')) + } + @{ + Value = '"1970-01-01T00:00:00.0000000Z"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).LocalDateTime) + } + ) { + param ($Value, $Expected) + + $json = $Value | ConvertFrom-Json + $json | Should -BeOfType ([DateTime]) + $json.Kind | Should -Be $Expected.Kind + $json | Should -Be $Expected + + $json = $Value | ConvertFrom-Json -DateKind Default + $json | Should -BeOfType ([DateTime]) + $json.Kind | Should -Be $Expected.Kind + $json | Should -Be $Expected + } + + It 'Parses DateKind Local strings for ' -TestCases @( + @{ + Value = '"2022-11-02T12:01:44.5801388+04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).LocalDateTime) + } + @{ + Value = '"2022-11-02T12:01:44.5801388-04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00"' + Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Local')) + } + @{ + Value = '"1970-01-01T00:00:00.0000000Z"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).LocalDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).LocalDateTime) + } + ) { + param ($Value, $Expected) + + $json = $Value | ConvertFrom-Json -DateKind Local + $json | Should -BeOfType ([DateTime]) + $json.Kind | Should -Be Local + $json | Should -Be $Expected + } + + It 'Parses DateKind Utc strings for ' -TestCases @( + @{ + Value = '"2022-11-02T12:01:44.5801388+04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8).UtcDateTime) + } + @{ + Value = '"2022-11-02T12:01:44.5801388-04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00"' + Expected = ([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Utc')) + } + @{ + Value = '"1970-01-01T00:00:00.0000000Z"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0)).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10)).UtcDateTime) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10)).UtcDateTime) + } + ) { + param ($Value, $Expected) + + $json = $Value | ConvertFrom-Json -DateKind Utc + $json | Should -BeOfType ([DateTime]) + $json.Kind | Should -Be Utc + $json | Should -Be $Expected + } + + It 'Parses DateKind Offset strings for ' -TestCases @( + @{ + Value = '"2022-11-02T12:01:44.5801388+04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours 4)).AddTicks(8)) + } + @{ + Value = '"2022-11-02T12:01:44.5801388-04:00"' + Expected = ([DateTimeOffset]::new(2022, 11, 2, 12, 1, 44, 580, 138, (New-TimeSpan -Hours -4)).AddTicks(8)) + } + @{ + Value = '"1970-01-01T00:00:00"' + Expected = ([DateTimeOffset]::new([DateTime]::new(1970, 1, 1, 0, 0, 0, 0, 0, 'Local'))) + } + @{ + Value = '"1970-01-01T00:00:00.0000000Z"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0))) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0))) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-00:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 0))) + } + @{ + Value = '"1970-01-01T00:00:00.0000000+10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours 10))) + } + @{ + Value = '"1970-01-01T00:00:00.0000000-10:00"' + Expected = ([DateTimeOffset]::new(1970, 1, 1, 0, 0, 0, 0, 0, (New-TimeSpan -Hours -10))) + } + ) { + param ($Value, $Expected) + + $json = $Value | ConvertFrom-Json -DateKind Offset + $json | Should -BeOfType ([DateTimeOffset]) + $json.EqualsExact($Expected) | Should -BeTrue + } + + It 'Parses DateKind String strings for ' -TestCases @( + @{ Value = '"2022-11-02T12:01:44.5801388+04:00"' } + @{ Value = '"2022-11-02T12:01:44.5801388-04:00"' } + @{ Value = '"1970-01-01T00:00:00"' } + @{ Value = '"1970-01-01T00:00:00.0000000Z"' } + @{ Value = '"1970-01-01T00:00:00.0000000+00:00"' } + @{ Value = '"1970-01-01T00:00:00.0000000-00:00"' } + @{ Value = '"1970-01-01T00:00:00.0000000+10:00"' } + @{ Value = '"1970-01-01T00:00:00.0000000-10:00"' } + ) { + param ($Value) + + $json = $Value | ConvertFrom-Json -DateKind String + $json | Should -BeOfType ([string]) + $json | Should -Be $Value.Substring(1, $Value.Length - 2) + } } Describe 'ConvertFrom-Json -Depth Tests' -tags "Feature" {