-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NRBF] More bug fixes #107682
[NRBF] More bug fixes #107682
Changes from 14 commits
5543848
48283cd
24166f5
bbaf39b
8bb78e8
af07e22
714b226
ac40734
34f96bb
9c8b0c6
2ff65c7
fdd59fc
6fca7f3
4ff4f82
3aa1656
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,17 +41,9 @@ internal ArraySinglePrimitiveRecord(ArrayInfo arrayInfo, IReadOnlyList<T> values | |
public override T[] GetArray(bool allowNulls = true) | ||
=> (T[])(_arrayNullsNotAllowed ??= (Values is T[] array ? array : Values.ToArray())); | ||
|
||
internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType() | ||
{ | ||
Debug.Fail("GetAllowedRecordType should never be called on ArraySinglePrimitiveRecord"); | ||
throw new InvalidOperationException(); | ||
} | ||
internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType() => throw new InvalidOperationException(); | ||
|
||
private protected override void AddValue(object value) | ||
{ | ||
Debug.Fail("AddValue should never be called on ArraySinglePrimitiveRecord"); | ||
throw new InvalidOperationException(); | ||
} | ||
private protected override void AddValue(object value) => throw new InvalidOperationException(); | ||
|
||
internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int count) | ||
{ | ||
|
@@ -94,7 +86,7 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c | |
#if NET | ||
reader.BaseStream.ReadExactly(resultAsBytes); | ||
#else | ||
byte[] bytes = ArrayPool<byte>.Shared.Rent(Math.Min(count * Unsafe.SizeOf<T>(), 256_000)); | ||
byte[] bytes = ArrayPool<byte>.Shared.Rent((int)Math.Min(requiredBytes, 256_000)); | ||
JeremyKuhne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
while (!resultAsBytes.IsEmpty) | ||
{ | ||
|
@@ -159,31 +151,10 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c | |
private static List<decimal> DecodeDecimals(BinaryReader reader, int count) | ||
{ | ||
List<decimal> values = new(); | ||
#if NET | ||
Span<byte> buffer = stackalloc byte[256]; | ||
for (int i = 0; i < count; i++) | ||
{ | ||
int stringLength = reader.Read7BitEncodedInt(); | ||
if (!(stringLength > 0 && stringLength <= buffer.Length)) | ||
{ | ||
ThrowHelper.ThrowInvalidValue(stringLength); | ||
} | ||
|
||
reader.BaseStream.ReadExactly(buffer.Slice(0, stringLength)); | ||
|
||
if (!decimal.TryParse(buffer.Slice(0, stringLength), NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) | ||
{ | ||
ThrowHelper.ThrowInvalidFormat(); | ||
} | ||
|
||
values.Add(value); | ||
} | ||
#else | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this perf optimization was wrong because it was not checking if given string input is a valid utf8 string |
||
for (int i = 0; i < count; i++) | ||
{ | ||
values.Add(reader.ParseDecimal()); | ||
} | ||
#endif | ||
return values; | ||
} | ||
|
||
|
@@ -244,12 +215,14 @@ private static List<T> DecodeFromNonSeekableStream(BinaryReader reader, int coun | |
{ | ||
values.Add((T)(object)Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadUInt64())); | ||
} | ||
else | ||
else if (typeof(T) == typeof(TimeSpan)) | ||
{ | ||
Debug.Assert(typeof(T) == typeof(TimeSpan)); | ||
|
||
values.Add((T)(object)new TimeSpan(reader.ReadInt64())); | ||
} | ||
else | ||
{ | ||
throw new InvalidOperationException(); | ||
} | ||
} | ||
|
||
return values; | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -53,10 +53,14 @@ internal static MemberTypeInfo Decode(BinaryReader reader, int count, PayloadOpt | |||||||||||||||
case BinaryType.Class: | ||||||||||||||||
info[i] = (type, ClassTypeInfo.Decode(reader, options, recordMap)); | ||||||||||||||||
break; | ||||||||||||||||
default: | ||||||||||||||||
// Other types have no additional data. | ||||||||||||||||
Debug.Assert(type is BinaryType.String or BinaryType.ObjectArray or BinaryType.StringArray or BinaryType.Object); | ||||||||||||||||
case BinaryType.String: | ||||||||||||||||
case BinaryType.StringArray: | ||||||||||||||||
case BinaryType.Object: | ||||||||||||||||
case BinaryType.ObjectArray: | ||||||||||||||||
// These types have no additional data. | ||||||||||||||||
break; | ||||||||||||||||
default: | ||||||||||||||||
throw new InvalidOperationException(); | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, InvalidOperationException is for when a method is invoked on an object and the object has state that doesn't support it. For example, calling Stream.Read when Stream.CanRead says false. The object is not in a state where you can call the method. Many of yours are probably more likely InvalidDataException, where the default message is "Found invalid data while decoding." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Immo corrected me. Stream.Read when Stream.CanRead is false is NotSupportedException (you should have checked the property on the stream). InvalidOperationException is for controllable state that was controlled wrong, like an object where you have to set 3 properties before calling "DoStuff", and you called DoStuff without setting all 3. But this is still most likely InvalidDataException, because the operation made sense to do, just the data that it read back was not internally consistent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I wanted to achieve is:
My initial idea was to use It's being tested by the Fuzzer which does not catch runtime/src/libraries/Fuzzing/DotnetFuzzing/Fuzzers/NrbfDecoderFuzzer.cs Lines 100 to 106 in 4930e1b
(as shown in the fuzzer comments in this PR that has discovered places where it was missing) |
||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
|
@@ -97,7 +101,8 @@ internal static MemberTypeInfo Decode(BinaryReader reader, int count, PayloadOpt | |||||||||||||||
BinaryType.PrimitiveArray => (PrimitiveArray, default), | ||||||||||||||||
BinaryType.Class => (NonSystemClass, default), | ||||||||||||||||
BinaryType.SystemClass => (SystemClass, default), | ||||||||||||||||
_ => (ObjectArray, default) | ||||||||||||||||
BinaryType.ObjectArray => (ObjectArray, default), | ||||||||||||||||
_ => throw new InvalidOperationException() | ||||||||||||||||
}; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
|
@@ -152,7 +157,7 @@ internal TypeName GetArrayTypeName(ArrayInfo arrayInfo) | |||||||||||||||
BinaryType.ObjectArray => TypeNameHelpers.GetPrimitiveSZArrayTypeName(TypeNameHelpers.ObjectPrimitiveType), | ||||||||||||||||
BinaryType.SystemClass => (TypeName)additionalInfo!, | ||||||||||||||||
BinaryType.Class => ((ClassTypeInfo)additionalInfo!).TypeName, | ||||||||||||||||
_ => throw new ArgumentOutOfRangeException(paramName: nameof(binaryType), actualValue: binaryType, message: null) | ||||||||||||||||
_ => throw new InvalidOperationException() | ||||||||||||||||
}; | ||||||||||||||||
|
||||||||||||||||
// In general, arrayRank == 1 may have two different meanings: | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,28 +69,22 @@ public static bool StartsWithPayloadHeader(Stream stream) | |
return false; | ||
} | ||
|
||
try | ||
byte[] buffer = new byte[SerializedStreamHeaderRecord.Size]; | ||
int offset = 0; | ||
while (offset < buffer.Length) | ||
{ | ||
#if NET | ||
Span<byte> buffer = stackalloc byte[SerializedStreamHeaderRecord.Size]; | ||
stream.ReadExactly(buffer); | ||
#else | ||
byte[] buffer = new byte[SerializedStreamHeaderRecord.Size]; | ||
int offset = 0; | ||
while (offset < buffer.Length) | ||
int read = stream.Read(buffer, offset, buffer.Length - offset); | ||
if (read == 0) | ||
{ | ||
int read = stream.Read(buffer, offset, buffer.Length - offset); | ||
if (read == 0) | ||
throw new EndOfStreamException(); | ||
offset += read; | ||
stream.Position = beginning; | ||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the goal of this change is to simply return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to set the stream position back to the beginning before we return here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Great catch! |
||
} | ||
#endif | ||
return StartsWithPayloadHeader(buffer); | ||
} | ||
finally | ||
{ | ||
stream.Position = beginning; | ||
offset += read; | ||
} | ||
|
||
bool result = StartsWithPayloadHeader(buffer); | ||
stream.Position = beginning; | ||
return result; | ||
} | ||
|
||
/// <summary> | ||
|
@@ -241,7 +235,8 @@ private static SerializationRecord DecodeNext(BinaryReader reader, RecordMap rec | |
SerializationRecordType.ObjectNullMultiple => ObjectNullMultipleRecord.Decode(reader), | ||
SerializationRecordType.ObjectNullMultiple256 => ObjectNullMultiple256Record.Decode(reader), | ||
SerializationRecordType.SerializedStreamHeader => SerializedStreamHeaderRecord.Decode(reader), | ||
_ => SystemClassWithMembersAndTypesRecord.Decode(reader, recordMap, options), | ||
SerializationRecordType.SystemClassWithMembersAndTypes => SystemClassWithMembersAndTypesRecord.Decode(reader, recordMap, options), | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
recordMap.Add(record); | ||
|
@@ -269,8 +264,10 @@ private static SerializationRecord DecodeMemberPrimitiveTypedRecord(BinaryReader | |
PrimitiveType.Double => new MemberPrimitiveTypedRecord<double>(reader.ReadDouble()), | ||
PrimitiveType.Decimal => new MemberPrimitiveTypedRecord<decimal>(reader.ParseDecimal()), | ||
PrimitiveType.DateTime => new MemberPrimitiveTypedRecord<DateTime>(Utils.BinaryReaderExtensions.CreateDateTimeFromData(reader.ReadUInt64())), | ||
// String is handled with a record, never on it's own | ||
_ => new MemberPrimitiveTypedRecord<TimeSpan>(new TimeSpan(reader.ReadInt64())), | ||
PrimitiveType.TimeSpan => new MemberPrimitiveTypedRecord<TimeSpan>(new TimeSpan(reader.ReadInt64())), | ||
// PrimitiveType.String is handled with a record, never on it's own | ||
PrimitiveType.String => throw new SerializationException(SR.Format(SR.Serialization_InvalidValue, primitiveType)), | ||
_ => throw new InvalidOperationException() | ||
}; | ||
} | ||
|
||
|
@@ -295,7 +292,10 @@ private static SerializationRecord DecodeArraySinglePrimitiveRecord(BinaryReader | |
PrimitiveType.Double => Decode<double>(info, reader), | ||
PrimitiveType.Decimal => Decode<decimal>(info, reader), | ||
PrimitiveType.DateTime => Decode<DateTime>(info, reader), | ||
_ => Decode<TimeSpan>(info, reader), | ||
PrimitiveType.TimeSpan => Decode<TimeSpan>(info, reader), | ||
// PrimitiveType.String is handled with a record, never on it's own | ||
PrimitiveType.String => throw new SerializationException(SR.Format(SR.Serialization_InvalidValue, primitiveType)), | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
static SerializationRecord Decode<T>(ArrayInfo info, BinaryReader reader) where T : unmanaged | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ namespace System.Formats.Nrbf; | |
internal sealed class RectangularArrayRecord : ArrayRecord | ||
{ | ||
private readonly int[] _lengths; | ||
private readonly ICollection<object> _values; | ||
private readonly List<object> _values; | ||
private TypeName? _typeName; | ||
|
||
private RectangularArrayRecord(Type elementType, ArrayInfo arrayInfo, | ||
|
@@ -24,18 +24,8 @@ private RectangularArrayRecord(Type elementType, ArrayInfo arrayInfo, | |
MemberTypeInfo = memberTypeInfo; | ||
_lengths = lengths; | ||
|
||
// A List<T> can hold as many objects as an array, so for multi-dimensional arrays | ||
// with more elements than Array.MaxLength we use LinkedList. | ||
// Testing that many elements takes a LOT of time, so to ensure that both code paths are tested, | ||
// we always use LinkedList code path for Debug builds. | ||
#if DEBUG | ||
_values = new LinkedList<object>(); | ||
#else | ||
_values = arrayInfo.TotalElementsCount <= ArrayInfo.MaxArrayLength | ||
? new List<object>(canPreAllocate ? arrayInfo.GetSZArrayLength() : Math.Min(4, arrayInfo.GetSZArrayLength())) | ||
: new LinkedList<object>(); | ||
#endif | ||
|
||
// ArrayInfo.GetSZArrayLength ensures to return a value <= Array.MaxLength | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since now we don't allow for any MD array with number of elements |
||
_values = new List<object>(canPreAllocate ? arrayInfo.GetSZArrayLength() : Math.Min(4, arrayInfo.GetSZArrayLength())); | ||
} | ||
|
||
public override SerializationRecordType RecordType => SerializationRecordType.BinaryArray; | ||
|
@@ -108,6 +98,7 @@ private protected override Array Deserialize(Type arrayType, bool allowNulls) | |
else if (ElementType == typeof(TimeSpan)) CopyTo<TimeSpan>(_values, result); | ||
else if (ElementType == typeof(DateTime)) CopyTo<DateTime>(_values, result); | ||
else if (ElementType == typeof(decimal)) CopyTo<decimal>(_values, result); | ||
else throw new InvalidOperationException(); | ||
} | ||
else | ||
{ | ||
|
@@ -116,7 +107,7 @@ private protected override Array Deserialize(Type arrayType, bool allowNulls) | |
|
||
return result; | ||
|
||
static void CopyTo<T>(ICollection<object> list, Array array) | ||
static void CopyTo<T>(List<object> list, Array array) | ||
{ | ||
ref byte arrayDataRef = ref MemoryMarshal.GetArrayDataReference(array); | ||
ref T firstElementRef = ref Unsafe.As<byte, T>(ref arrayDataRef); | ||
|
@@ -176,7 +167,11 @@ internal static RectangularArrayRecord Create(BinaryReader reader, ArrayInfo arr | |
PrimitiveType.Int64 => sizeof(long), | ||
PrimitiveType.UInt64 => sizeof(ulong), | ||
PrimitiveType.Double => sizeof(double), | ||
_ => -1 | ||
PrimitiveType.TimeSpan => sizeof(ulong), | ||
PrimitiveType.DateTime => sizeof(ulong), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an improvement that I got into when I was making all the |
||
PrimitiveType.Decimal => -1, // represented as variable-length string | ||
PrimitiveType.String => -1, // can't predict required size based on the number of strings | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
if (sizeOfSingleValue > 0) | ||
|
@@ -215,7 +210,8 @@ private static Type MapPrimitive(PrimitiveType primitiveType) | |
PrimitiveType.DateTime => typeof(DateTime), | ||
PrimitiveType.UInt16 => typeof(ushort), | ||
PrimitiveType.UInt32 => typeof(uint), | ||
_ => typeof(ulong) | ||
PrimitiveType.UInt64 => typeof(ulong), | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
private static Type MapPrimitiveArray(PrimitiveType primitiveType) | ||
|
@@ -235,7 +231,8 @@ private static Type MapPrimitiveArray(PrimitiveType primitiveType) | |
PrimitiveType.DateTime => typeof(DateTime[]), | ||
PrimitiveType.UInt16 => typeof(ushort[]), | ||
PrimitiveType.UInt32 => typeof(uint[]), | ||
_ => typeof(ulong[]), | ||
PrimitiveType.UInt64 => typeof(ulong[]), | ||
_ => throw new InvalidOperationException() | ||
}; | ||
|
||
private static object? GetActualValue(object value) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Formats.Nrbf.Utils; | ||
using System.IO; | ||
using System.Linq; | ||
|
@@ -15,6 +16,7 @@ namespace System.Formats.Nrbf; | |
/// <summary> | ||
/// The ID of <see cref="SerializationRecord" />. | ||
/// </summary> | ||
[DebuggerDisplay("{_id}")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just user experience improvement |
||
public readonly struct SerializationRecordId : IEquatable<SerializationRecordId> | ||
{ | ||
#pragma warning disable CS0649 // the default value is used on purpose | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type no longer provides
Length
property.