Skip to content

Commit 8b36047

Browse files
authored
Merge pull request #47 from joadan/copilot/guard-against-datetime-range
Guard DateTime/DateTimeOffset converters against out-of-range Unix milliseconds
2 parents 723d981 + a3b7034 commit 8b36047

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/Linq2OData.Core/Converters/MicrosoftDateTimeConverter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ internal static class MicrosoftDateTimeHelper
1111
new Regex(@"^/Date\((\-?\d+)(?:[+-]\d{4})?\)/$",
1212
RegexOptions.Compiled);
1313

14+
internal const long MinMilliseconds = -62135596800000L;
15+
internal const long MaxMilliseconds = 253402300799999L;
16+
1417
internal static DateTime ParseDateTime(string value)
1518
{
1619
if (string.IsNullOrWhiteSpace(value))
@@ -22,6 +25,11 @@ internal static DateTime ParseDateTime(string value)
2225

2326
var milliseconds = long.Parse(match.Groups[1].Value);
2427

28+
if (milliseconds < MinMilliseconds)
29+
return DateTime.MinValue;
30+
if (milliseconds > MaxMilliseconds)
31+
return DateTime.MaxValue;
32+
2533
// IMPORTANT: treat as UTC (matches old .NET behavior)
2634
return DateTimeOffset
2735
.FromUnixTimeMilliseconds(milliseconds)

src/Linq2OData.Core/Converters/MicrosoftDateTimeOffsetConverter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public override DateTimeOffset Read(
2929
throw new JsonException($"Invalid date format: {value}");
3030

3131
var milliseconds = long.Parse(match.Groups[1].Value);
32+
33+
if (milliseconds < MicrosoftDateTimeHelper.MinMilliseconds)
34+
return DateTimeOffset.MinValue;
35+
if (milliseconds > MicrosoftDateTimeHelper.MaxMilliseconds)
36+
return DateTimeOffset.MaxValue;
37+
3238
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds);
3339
}
3440

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Linq2OData.Core.Converters;
2+
using System.Text.Json;
3+
using Xunit;
4+
5+
namespace Linq2OData.Tests
6+
{
7+
public class MicrosoftDateTimeConverterTests
8+
{
9+
private static JsonSerializerOptions DateTimeOptions()
10+
{
11+
var options = new JsonSerializerOptions();
12+
options.Converters.Add(new MicrosoftDateTimeConverter());
13+
return options;
14+
}
15+
16+
private static JsonSerializerOptions NullableDateTimeOptions()
17+
{
18+
var options = new JsonSerializerOptions();
19+
options.Converters.Add(new MicrosoftNullableDateTimeConverter());
20+
return options;
21+
}
22+
23+
private static JsonSerializerOptions DateTimeOffsetOptions()
24+
{
25+
var options = new JsonSerializerOptions();
26+
options.Converters.Add(new MicrosoftDateTimeOffsetConverter());
27+
return options;
28+
}
29+
30+
[Fact]
31+
public void Read_NormalValue_ShouldParse()
32+
{
33+
var json = "\"/Date(970358400000)/\"";
34+
var result = JsonSerializer.Deserialize<DateTime>(json, DateTimeOptions());
35+
Assert.Equal(new DateTime(2000, 10, 1, 0, 0, 0, DateTimeKind.Utc), result);
36+
}
37+
38+
[Fact]
39+
public void Read_BelowMinRange_ShouldReturnMinValue()
40+
{
41+
// -62135769600000 is below the valid minimum of -62135596800000
42+
var json = "\"/Date(-62135769600000)/\"";
43+
var result = JsonSerializer.Deserialize<DateTime>(json, DateTimeOptions());
44+
Assert.Equal(DateTime.MinValue, result);
45+
}
46+
47+
[Fact]
48+
public void Read_AboveMaxRange_ShouldReturnMaxValue()
49+
{
50+
// 253402300800000 is above the valid maximum of 253402300799999
51+
var json = "\"/Date(253402300800000)/\"";
52+
var result = JsonSerializer.Deserialize<DateTime>(json, DateTimeOptions());
53+
Assert.Equal(DateTime.MaxValue, result);
54+
}
55+
56+
[Fact]
57+
public void Read_ExactMinRange_ShouldParse()
58+
{
59+
var json = "\"/Date(-62135596800000)/\"";
60+
var result = JsonSerializer.Deserialize<DateTime>(json, DateTimeOptions());
61+
Assert.Equal(DateTime.MinValue.Date, result.Date);
62+
}
63+
64+
[Fact]
65+
public void NullableRead_BelowMinRange_ShouldReturnMinValue()
66+
{
67+
var json = "\"/Date(-62135769600000)/\"";
68+
var result = JsonSerializer.Deserialize<DateTime?>(json, NullableDateTimeOptions());
69+
Assert.Equal(DateTime.MinValue, result);
70+
}
71+
72+
[Fact]
73+
public void NullableRead_AboveMaxRange_ShouldReturnMaxValue()
74+
{
75+
var json = "\"/Date(253402300800000)/\"";
76+
var result = JsonSerializer.Deserialize<DateTime?>(json, NullableDateTimeOptions());
77+
Assert.Equal(DateTime.MaxValue, result);
78+
}
79+
80+
[Fact]
81+
public void DateTimeOffset_Read_BelowMinRange_ShouldReturnMinValue()
82+
{
83+
var json = "\"/Date(-62135769600000)/\"";
84+
var result = JsonSerializer.Deserialize<DateTimeOffset>(json, DateTimeOffsetOptions());
85+
Assert.Equal(DateTimeOffset.MinValue, result);
86+
}
87+
88+
[Fact]
89+
public void DateTimeOffset_Read_AboveMaxRange_ShouldReturnMaxValue()
90+
{
91+
var json = "\"/Date(253402300800000)/\"";
92+
var result = JsonSerializer.Deserialize<DateTimeOffset>(json, DateTimeOffsetOptions());
93+
Assert.Equal(DateTimeOffset.MaxValue, result);
94+
}
95+
96+
[Fact]
97+
public void DateTimeOffset_Read_NormalValue_ShouldParse()
98+
{
99+
var json = "\"/Date(970358400000)/\"";
100+
var result = JsonSerializer.Deserialize<DateTimeOffset>(json, DateTimeOffsetOptions());
101+
Assert.Equal(new DateTimeOffset(2000, 10, 1, 0, 0, 0, TimeSpan.Zero), result);
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)