Skip to content

Commit 38709f8

Browse files
authored
Merge branch 'release/9.0' into release/9.0-staging
2 parents 624638d + cc58c28 commit 38709f8

37 files changed

+1736
-838
lines changed

src/libraries/System.Formats.Nrbf/ref/System.Formats.Nrbf.cs

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ namespace System.Formats.Nrbf
99
public abstract partial class ArrayRecord : System.Formats.Nrbf.SerializationRecord
1010
{
1111
internal ArrayRecord() { }
12-
public virtual long FlattenedLength { get { throw null; } }
1312
public override System.Formats.Nrbf.SerializationRecordId Id { get { throw null; } }
1413
public abstract System.ReadOnlySpan<int> Lengths { get; }
1514
public int Rank { get { throw null; } }

src/libraries/System.Formats.Nrbf/src/PACKAGE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ There are more than a dozen different serialization [record types](https://learn
5454
- `PrimitiveTypeRecord<T>` derives from the non-generic [PrimitiveTypeRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.primitivetyperecord), which also exposes a [Value](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.primitivetyperecord.value) property. But on the base class, the value is returned as `object` (which introduces boxing for value types).
5555
- [ClassRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.classrecord): describes all `class` and `struct` besides the aforementioned primitive types.
5656
- [ArrayRecord](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.arrayrecord): describes all array records, including jagged and multi-dimensional arrays.
57-
- [`SZArrayRecord<T>`](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.szarrayrecord-1): describes single-dimensional, zero-indexed array records, where `T` can be either a primitive type or a `ClassRecord`.
57+
- [`SZArrayRecord<T>`](https://learn.microsoft.com/dotnet/api/system.formats.nrbf.szarrayrecord-1): describes single-dimensional, zero-indexed array records, where `T` can be either a primitive type or a `SerializationRecord`.
5858

5959
```csharp
6060
SerializationRecord rootObject = NrbfDecoder.Decode(payload); // payload is a Stream

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/AllowedRecordType.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ internal enum AllowedRecordTypes : uint
2828
ArraySingleString = 1 << SerializationRecordType.ArraySingleString,
2929

3030
Nulls = ObjectNull | ObjectNullMultiple256 | ObjectNullMultiple,
31+
Arrays = ArraySingleObject | ArraySinglePrimitive | ArraySingleString | BinaryArray,
3132

3233
/// <summary>
3334
/// Any .NET object (a primitive, a reference type, a reference or single null).
3435
/// </summary>
3536
AnyObject = MemberPrimitiveTyped
36-
| ArraySingleObject | ArraySinglePrimitive | ArraySingleString | BinaryArray
37+
| Arrays
3738
| ClassWithId | ClassWithMembersAndTypes | SystemClassWithMembersAndTypes
3839
| BinaryObjectString
3940
| MemberReference

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArrayRecord.cs

+85-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Reflection.Metadata;
66
using System.Formats.Nrbf.Utils;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Runtime.Serialization;
710

811
namespace System.Formats.Nrbf;
912

@@ -27,12 +30,6 @@ private protected ArrayRecord(ArrayInfo arrayInfo)
2730
/// <value>A buffer of integers that represent the number of elements in every dimension.</value>
2831
public abstract ReadOnlySpan<int> Lengths { get; }
2932

30-
/// <summary>
31-
/// When overridden in a derived class, gets the total number of all elements in every dimension.
32-
/// </summary>
33-
/// <value>A number that represent the total number of all elements in every dimension.</value>
34-
public virtual long FlattenedLength => ArrayInfo.FlattenedLength;
35-
3633
/// <summary>
3734
/// Gets the rank of the array.
3835
/// </summary>
@@ -118,4 +115,86 @@ private void HandleNext(object value, NextInfo info, int size)
118115
}
119116

120117
internal abstract (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType();
118+
119+
internal static void Populate(List<SerializationRecord> source, Array destination, int[] lengths, AllowedRecordTypes allowedRecordTypes, bool allowNulls)
120+
{
121+
int[] indices = new int[lengths.Length];
122+
nuint numElementsWritten = 0; // only for debugging; not used in release builds
123+
124+
foreach (SerializationRecord record in source)
125+
{
126+
object? value = GetActualValue(record, allowedRecordTypes, out int incrementCount);
127+
if (value is not null)
128+
{
129+
// null is a default element for all array of reference types, so we don't call SetValue for nulls.
130+
destination.SetValue(value, indices);
131+
Debug.Assert(incrementCount == 1, "IncrementCount other than 1 is allowed only for null records.");
132+
}
133+
else if (!allowNulls)
134+
{
135+
ThrowHelper.ThrowArrayContainedNulls();
136+
}
137+
138+
while (incrementCount > 0)
139+
{
140+
incrementCount--;
141+
numElementsWritten++;
142+
int dimension = indices.Length - 1;
143+
while (dimension >= 0)
144+
{
145+
indices[dimension]++;
146+
if (indices[dimension] < lengths[dimension])
147+
{
148+
break;
149+
}
150+
indices[dimension] = 0;
151+
dimension--;
152+
}
153+
154+
if (dimension < 0)
155+
{
156+
break;
157+
}
158+
}
159+
}
160+
161+
Debug.Assert(numElementsWritten == (uint)source.Count, "We should have traversed the entirety of the source records collection.");
162+
Debug.Assert(numElementsWritten == (ulong)destination.LongLength, "We should have traversed the entirety of the destination array.");
163+
}
164+
165+
private static object? GetActualValue(SerializationRecord record, AllowedRecordTypes allowedRecordTypes, out int repeatCount)
166+
{
167+
repeatCount = 1;
168+
169+
if (record is NullsRecord nullsRecord)
170+
{
171+
repeatCount = nullsRecord.NullCount;
172+
return null;
173+
}
174+
else if (record.RecordType == SerializationRecordType.MemberReference)
175+
{
176+
record = ((MemberReferenceRecord)record).GetReferencedRecord();
177+
}
178+
179+
if (allowedRecordTypes == AllowedRecordTypes.BinaryObjectString)
180+
{
181+
if (record is not BinaryObjectStringRecord stringRecord)
182+
{
183+
throw new SerializationException(SR.Serialization_InvalidReference);
184+
}
185+
186+
return stringRecord.Value;
187+
}
188+
else if (allowedRecordTypes == AllowedRecordTypes.Arrays)
189+
{
190+
if (record is not ArrayRecord arrayRecord)
191+
{
192+
throw new SerializationException(SR.Serialization_InvalidReference);
193+
}
194+
195+
return arrayRecord;
196+
}
197+
198+
return record;
199+
}
121200
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Formats.Nrbf.Utils;
9+
using System.Linq;
10+
using System.Reflection.Metadata;
11+
using System.Runtime.CompilerServices;
12+
using System.Runtime.InteropServices;
13+
using System.Text;
14+
using System.Threading.Tasks;
15+
16+
namespace System.Formats.Nrbf
17+
{
18+
internal sealed class ArrayRectangularPrimitiveRecord<T> : ArrayRecord where T : unmanaged
19+
{
20+
private readonly int[] _lengths;
21+
private readonly IReadOnlyList<T> _values;
22+
private TypeName? _typeName;
23+
24+
internal ArrayRectangularPrimitiveRecord(ArrayInfo arrayInfo, int[] lengths, IReadOnlyList<T> values) : base(arrayInfo)
25+
{
26+
_lengths = lengths;
27+
_values = values;
28+
ValuesToRead = 0; // there is nothing to read anymore
29+
}
30+
31+
public override ReadOnlySpan<int> Lengths => _lengths;
32+
33+
public override SerializationRecordType RecordType => SerializationRecordType.BinaryArray;
34+
35+
public override TypeName TypeName
36+
=> _typeName ??= TypeNameHelpers.GetPrimitiveTypeName(TypeNameHelpers.GetPrimitiveType<T>()).MakeArrayTypeName(Rank);
37+
38+
internal override (AllowedRecordTypes allowed, PrimitiveType primitiveType) GetAllowedRecordType() => throw new InvalidOperationException();
39+
40+
private protected override void AddValue(object value) => throw new InvalidOperationException();
41+
42+
[RequiresDynamicCode("May call Array.CreateInstance().")]
43+
private protected override Array Deserialize(Type arrayType, bool allowNulls)
44+
{
45+
Array result =
46+
#if NET9_0_OR_GREATER
47+
Array.CreateInstanceFromArrayType(arrayType, _lengths);
48+
#else
49+
Array.CreateInstance(typeof(T), _lengths);
50+
#endif
51+
int[] indices = new int[_lengths.Length];
52+
nuint numElementsWritten = 0; // only for debugging; not used in release builds
53+
54+
for (int i = 0; i < _values.Count; i++)
55+
{
56+
result.SetValue(_values[i], indices);
57+
numElementsWritten++;
58+
59+
int dimension = indices.Length - 1;
60+
while (dimension >= 0)
61+
{
62+
indices[dimension]++;
63+
if (indices[dimension] < Lengths[dimension])
64+
{
65+
break;
66+
}
67+
indices[dimension] = 0;
68+
dimension--;
69+
}
70+
71+
if (dimension < 0)
72+
{
73+
break;
74+
}
75+
}
76+
77+
Debug.Assert(numElementsWritten == (uint)_values.Count, "We should have traversed the entirety of the source values collection.");
78+
Debug.Assert(numElementsWritten == (ulong)result.LongLength, "We should have traversed the entirety of the destination array.");
79+
80+
return result;
81+
}
82+
}
83+
}

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySingleObjectRecord.cs

+14-12
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ namespace System.Formats.Nrbf;
1515
/// <remarks>
1616
/// ArraySingleObject records are described in <see href="https://learn.microsoft.com/openspecs/windows_protocols/ms-nrbf/982b2f50-6367-402a-aaf2-44ee96e2a5e0">[MS-NRBF] 2.4.3.2</see>.
1717
/// </remarks>
18-
internal sealed class ArraySingleObjectRecord : SZArrayRecord<object?>
18+
internal sealed class ArraySingleObjectRecord : SZArrayRecord<SerializationRecord>
1919
{
20-
private ArraySingleObjectRecord(ArrayInfo arrayInfo) : base(arrayInfo) => Records = [];
20+
internal ArraySingleObjectRecord(ArrayInfo arrayInfo) : base(arrayInfo) => Records = [];
2121

2222
public override SerializationRecordType RecordType => SerializationRecordType.ArraySingleObject;
2323

@@ -27,25 +27,26 @@ public override TypeName TypeName
2727
private List<SerializationRecord> Records { get; }
2828

2929
/// <inheritdoc/>
30-
public override object?[] GetArray(bool allowNulls = true)
31-
=> (object?[])(allowNulls ? _arrayNullsAllowed ??= ToArray(true) : _arrayNullsNotAllowed ??= ToArray(false));
30+
public override SerializationRecord?[] GetArray(bool allowNulls = true)
31+
=> (SerializationRecord?[])(allowNulls ? _arrayNullsAllowed ??= ToArray(true) : _arrayNullsNotAllowed ??= ToArray(false));
3232

33-
private object?[] ToArray(bool allowNulls)
33+
private SerializationRecord?[] ToArray(bool allowNulls)
3434
{
35-
object?[] values = new object?[Length];
35+
SerializationRecord?[] values = new SerializationRecord?[Length];
3636

3737
int valueIndex = 0;
3838
for (int recordIndex = 0; recordIndex < Records.Count; recordIndex++)
3939
{
4040
SerializationRecord record = Records[recordIndex];
4141

42-
int nullCount = record is NullsRecord nullsRecord ? nullsRecord.NullCount : 0;
43-
if (nullCount == 0)
42+
if (record is MemberReferenceRecord referenceRecord)
4443
{
45-
// "new object[] { <SELF> }" is special cased because it allows for storing reference to itself.
46-
values[valueIndex++] = record is MemberReferenceRecord referenceRecord && referenceRecord.Reference.Equals(Id)
47-
? values // a reference to self, and a way to get StackOverflow exception ;)
48-
: record.GetValue();
44+
record = referenceRecord.GetReferencedRecord();
45+
}
46+
47+
if (record is not NullsRecord nullsRecord)
48+
{
49+
values[valueIndex++] = record;
4950
continue;
5051
}
5152

@@ -54,6 +55,7 @@ public override TypeName TypeName
5455
ThrowHelper.ThrowArrayContainedNulls();
5556
}
5657

58+
int nullCount = nullsRecord.NullCount;
5759
do
5860
{
5961
values[valueIndex++] = null;

src/libraries/System.Formats.Nrbf/src/System/Formats/Nrbf/ArraySinglePrimitiveRecord.cs

+50-25
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public override T[] GetArray(bool allowNulls = true)
4747

4848
internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int count)
4949
{
50+
if (count == 0)
51+
{
52+
return Array.Empty<T>(); // Empty arrays are allowed.
53+
}
54+
5055
// For decimals, the input is provided as strings, so we can't compute the required size up-front.
5156
if (typeof(T) == typeof(decimal))
5257
{
@@ -71,18 +76,15 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
7176
// allocations to be proportional to the amount of data present in the input stream,
7277
// which is a sufficient defense against DoS.
7378

74-
long requiredBytes = count;
75-
if (typeof(T) == typeof(DateTime) || typeof(T) == typeof(TimeSpan))
76-
{
77-
// We can't assume DateTime as represented by the runtime is 8 bytes.
78-
// The only assumption we can make is that it's 8 bytes on the wire.
79-
requiredBytes *= 8;
80-
}
81-
else if (typeof(T) != typeof(char))
82-
{
83-
requiredBytes *= Unsafe.SizeOf<T>();
84-
}
79+
// We can't assume DateTime as represented by the runtime is 8 bytes.
80+
// The only assumption we can make is that it's 8 bytes on the wire.
81+
int sizeOfT = typeof(T) == typeof(DateTime) || typeof(T) == typeof(TimeSpan)
82+
? 8
83+
: typeof(T) != typeof(char)
84+
? Unsafe.SizeOf<T>()
85+
: 1;
8586

87+
long requiredBytes = (long)count * sizeOfT;
8688
bool? isDataAvailable = reader.IsDataAvailable(requiredBytes);
8789
if (!isDataAvailable.HasValue)
8890
{
@@ -110,26 +112,49 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
110112

111113
// It's safe to pre-allocate, as we have ensured there is enough bytes in the stream.
112114
T[] result = new T[count];
113-
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(result);
114-
#if NET
115-
reader.BaseStream.ReadExactly(resultAsBytes);
115+
116+
// MemoryMarshal.AsBytes can fail for inputs that need more than int.MaxValue bytes.
117+
// To avoid OverflowException, we read the data in chunks.
118+
int MaxChunkLength =
119+
#if !DEBUG
120+
int.MaxValue / sizeOfT;
116121
#else
117-
byte[] bytes = ArrayPool<byte>.Shared.Rent((int)Math.Min(requiredBytes, 256_000));
122+
// Let's use a different value for non-release builds to ensure this code path
123+
// is covered with tests without the need of decoding enormous payloads.
124+
8_000;
125+
#endif
118126

119-
while (!resultAsBytes.IsEmpty)
127+
#if !NET
128+
byte[] rented = ArrayPool<byte>.Shared.Rent((int)Math.Min(requiredBytes, 256_000));
129+
#endif
130+
131+
Span<T> valuesToRead = result.AsSpan();
132+
while (!valuesToRead.IsEmpty)
120133
{
121-
int bytesRead = reader.Read(bytes, 0, Math.Min(resultAsBytes.Length, bytes.Length));
122-
if (bytesRead <= 0)
134+
int sliceSize = Math.Min(valuesToRead.Length, MaxChunkLength);
135+
136+
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(valuesToRead.Slice(0, sliceSize));
137+
#if NET
138+
reader.BaseStream.ReadExactly(resultAsBytes);
139+
#else
140+
while (!resultAsBytes.IsEmpty)
123141
{
124-
ArrayPool<byte>.Shared.Return(bytes);
125-
ThrowHelper.ThrowEndOfStreamException();
126-
}
142+
int bytesRead = reader.Read(rented, 0, Math.Min(resultAsBytes.Length, rented.Length));
143+
if (bytesRead <= 0)
144+
{
145+
ArrayPool<byte>.Shared.Return(rented);
146+
ThrowHelper.ThrowEndOfStreamException();
147+
}
127148

128-
bytes.AsSpan(0, bytesRead).CopyTo(resultAsBytes);
129-
resultAsBytes = resultAsBytes.Slice(bytesRead);
149+
rented.AsSpan(0, bytesRead).CopyTo(resultAsBytes);
150+
resultAsBytes = resultAsBytes.Slice(bytesRead);
151+
}
152+
#endif
153+
valuesToRead = valuesToRead.Slice(sliceSize);
130154
}
131155

132-
ArrayPool<byte>.Shared.Return(bytes);
156+
#if !NET
157+
ArrayPool<byte>.Shared.Return(rented);
133158
#endif
134159

135160
if (!BitConverter.IsLittleEndian)
@@ -176,7 +201,7 @@ internal static IReadOnlyList<T> DecodePrimitiveTypes(BinaryReader reader, int c
176201
{
177202
// See DontCastBytesToBooleans test to see what could go wrong.
178203
bool[] booleans = (bool[])(object)result;
179-
resultAsBytes = MemoryMarshal.AsBytes<T>(result);
204+
Span<byte> resultAsBytes = MemoryMarshal.AsBytes<T>(result);
180205
for (int i = 0; i < booleans.Length; i++)
181206
{
182207
// We don't use the bool array to get the value, as an optimizing compiler or JIT could elide this.

0 commit comments

Comments
 (0)