Skip to content

Commit ebe4398

Browse files
russcamgithub-actions[bot]
authored andcommitted
Fix bug in IsLong implementation (#4655)
Relates: #4649 This commit fixes a bug in the IsLong implementation, and prefers reading long values over double values in PrimitiveObjectFormatter when the bytes represent a valid long value, to avoid floating point rounding errors that may happen when parsing a double from a long value. The bug in IsLong was that the loop over the array segment bytes was exiting early when the count of bytes was the same as long.Min/MaxValue length, and the current loop byte is less than the byte at index in long.Min/MaxValue. In this scenario, it should not exit early, but not continue to check following loop bytes against byte at index in long.Min/MaxValue. Following loop iterations still need to check whether remaining bytes are numbers.
1 parent 322be78 commit ebe4398

File tree

3 files changed

+97
-10
lines changed

3 files changed

+97
-10
lines changed

src/Elasticsearch.Net/Extensions/ArraySegmentBytesExtensions.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Globalization;
23
using System.Runtime.CompilerServices;
34
using Elasticsearch.Net.Utf8Json;
45
using Elasticsearch.Net.Utf8Json.Formatters;
@@ -29,9 +30,9 @@ public static bool IsDouble(this ref ArraySegment<byte> arraySegment)
2930
return false;
3031
}
3132

32-
private static readonly byte[] LongMaxValue = StringEncoding.UTF8.GetBytes(long.MaxValue.ToString());
33+
private static readonly byte[] LongMaxValue = StringEncoding.UTF8.GetBytes(long.MaxValue.ToString(CultureInfo.InvariantCulture));
3334

34-
private static readonly byte[] LongMinValue = StringEncoding.UTF8.GetBytes(long.MinValue.ToString());
35+
private static readonly byte[] LongMinValue = StringEncoding.UTF8.GetBytes(long.MinValue.ToString(CultureInfo.InvariantCulture));
3536

3637
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3738
public static bool IsLong(this ref ArraySegment<byte> arraySegment)
@@ -45,22 +46,27 @@ public static bool IsLong(this ref ArraySegment<byte> arraySegment)
4546
return false;
4647

4748
var longBytes = isNegative ? LongMinValue : LongMaxValue;
48-
var i = isNegative ? 1 : 0;
4949

50+
// this doesn't handle positive values that are prefixed with + symbol.
51+
// Elasticsearch does not return values with this prefix.
52+
var i = isNegative ? 1 : 0;
53+
var check = arraySegment.Count == longBytes.Length;
5054
while (i < arraySegment.Count)
5155
{
5256
var b = arraySegment.Array[arraySegment.Offset + i];
5357
if (!NumberConverter.IsNumber(b))
5458
return false;
5559

5660
// only compare to long.MinValue or long.MaxValue if we're dealing with same number of bytes
57-
if (arraySegment.Count == longBytes.Length)
61+
if (check)
5862
{
63+
// larger than long.MinValue or long.MaxValue, bail early.
5964
if (b > longBytes[i])
6065
return false;
6166

67+
// only continue to check bytes if current byte is equal to longBytes[i]
6268
if (b < longBytes[i])
63-
return true;
69+
check = false;
6470
}
6571

6672
i++;

src/Elasticsearch.Net/Utf8Json/Formatters/PrimitiveObjectFormatter.cs

+2-5
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public void Serialize(ref JsonWriter writer, object value, IJsonFormatterResolve
9090

9191
if (t.IsEnum)
9292
{
93-
writer.WriteString(t.ToString()); // enum as stringq
93+
writer.WriteString(t.ToString()); // enum as string
9494
return;
9595
}
9696

@@ -158,13 +158,10 @@ public object Deserialize(ref JsonReader reader, IJsonFormatterResolver formatte
158158
case JsonToken.Number:
159159
var numberSegment = reader.ReadNumberSegment();
160160
// conditional operator here would cast both to double, so don't use.
161-
// Check for IsDouble first, IsDouble && IsLong can both return true, prefer precision
162-
if (numberSegment.IsDouble())
163-
return NumberConverter.ReadDouble(numberSegment.Array, numberSegment.Offset, out _);
161+
// Check for IsLong first, avoid floating point rounding
164162
if (numberSegment.IsLong())
165163
return NumberConverter.ReadInt64(numberSegment.Array, numberSegment.Offset, out _);
166164

167-
168165
return NumberConverter.ReadDouble(numberSegment.Array, numberSegment.Offset, out _);
169166
case JsonToken.String:
170167
return reader.ReadString();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Text;
2+
using Elastic.Xunit.XunitPlumbing;
3+
using Elasticsearch.Net;
4+
using FluentAssertions;
5+
using Tests.Core.Client;
6+
7+
namespace Tests.CodeStandards.Serialization
8+
{
9+
public class PrimitiveObjectFormatterTests
10+
{
11+
private static DynamicValue GetValue(string json)
12+
{
13+
var bytes = Encoding.UTF8.GetBytes(json);
14+
var client = TestClient.FixedInMemoryClient(bytes);
15+
var response = client.LowLevel.Search<DynamicResponse>(PostData.Empty);
16+
return response.Body["value"];
17+
}
18+
19+
[U]
20+
public void ReadDoubleWhenContainsDecimalPoint()
21+
{
22+
var value = GetValue(@"{""value"":0.43066659210472646}");
23+
value.Value.GetType().Should().Be<double>();
24+
value.Value.Should().Be(0.43066659210472646);
25+
}
26+
27+
[U]
28+
public void ReadDoubleWhenContainsENotation()
29+
{
30+
var value = GetValue(@"{""value"":1.7976931348623157E+308}");
31+
value.Value.GetType().Should().Be<double>();
32+
value.Value.Should().Be(double.MaxValue);
33+
}
34+
35+
[U]
36+
public void ReadDoubleWhenGreaterThanLongMaxValue()
37+
{
38+
var value = GetValue($"{{\"value\":{((double)long.MaxValue) + 1}}}");
39+
value.Value.GetType().Should().Be<double>();
40+
value.Value.Should().Be(((double)long.MaxValue) + 1);
41+
}
42+
43+
[U]
44+
public void ReadDoubleWhenLessThanLongMinValue()
45+
{
46+
var value = GetValue($"{{\"value\":{((double)long.MinValue) - 1}}}");
47+
value.Value.GetType().Should().Be<double>();
48+
value.Value.Should().Be(((double)long.MinValue) - 1);
49+
}
50+
51+
[U]
52+
public void ReadLongWhenLongMaxValue()
53+
{
54+
var value = GetValue($"{{\"value\":{long.MaxValue}}}");
55+
value.Value.GetType().Should().Be<long>();
56+
value.Value.Should().Be(long.MaxValue);
57+
}
58+
59+
[U]
60+
public void ReadLongWhenLessThanLongMaxValue()
61+
{
62+
var value = GetValue($"{{\"value\":{long.MaxValue - 1}}}");
63+
value.Value.GetType().Should().Be<long>();
64+
value.Value.Should().Be(long.MaxValue - 1);
65+
}
66+
67+
[U]
68+
public void ReadLongWhenGreaterThanLongMinValue()
69+
{
70+
var value = GetValue($"{{\"value\":{long.MinValue + 1}}}");
71+
value.Value.GetType().Should().Be<long>();
72+
value.Value.Should().Be(long.MinValue + 1);
73+
}
74+
75+
[U]
76+
public void ReadLongWhenLongMinValue()
77+
{
78+
var value = GetValue($"{{\"value\":{long.MinValue}}}");
79+
value.Value.GetType().Should().Be<long>();
80+
value.Value.Should().Be(long.MinValue);
81+
}
82+
}
83+
}
84+

0 commit comments

Comments
 (0)