|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace System.Text.Json
{
public sealed partial class JsonDocument
{
// Cached unrented documents for literal values.
private static JsonDocument? s_nullLiteral;
private static JsonDocument? s_trueLiteral;
private static JsonDocument? s_falseLiteral;
private const int UnseekableStreamInitialRentSize = 4096;
/// <summary>
/// Parse memory as UTF-8 encoded text representing a single JSON value into a JsonDocument.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="ReadOnlyMemory{T}"/> value will be used for the entire lifetime of the
/// JsonDocument object, and the caller must ensure that the data therein does not change during
/// the object lifetime.
/// </para>
///
/// <para>
/// Because the input is considered to be text, a UTF-8 Byte-Order-Mark (BOM) must not be present.
/// </para>
/// </remarks>
/// <param name="utf8Json">JSON text to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <returns>
/// A JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="utf8Json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static JsonDocument Parse(ReadOnlyMemory<byte> utf8Json, JsonDocumentOptions options = default)
{
return Parse(utf8Json, options.GetReaderOptions());
}
/// <summary>
/// Parse a sequence as UTF-8 encoded text representing a single JSON value into a JsonDocument.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="ReadOnlySequence{T}"/> may be used for the entire lifetime of the
/// JsonDocument object, and the caller must ensure that the data therein does not change during
/// the object lifetime.
/// </para>
///
/// <para>
/// Because the input is considered to be text, a UTF-8 Byte-Order-Mark (BOM) must not be present.
/// </para>
/// </remarks>
/// <param name="utf8Json">JSON text to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <returns>
/// A JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="utf8Json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static JsonDocument Parse(ReadOnlySequence<byte> utf8Json, JsonDocumentOptions options = default)
{
JsonReaderOptions readerOptions = options.GetReaderOptions();
if (utf8Json.IsSingleSegment)
{
return Parse(utf8Json.First, readerOptions);
}
int length = checked((int)utf8Json.Length);
byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(length);
try
{
utf8Json.CopyTo(utf8Bytes.AsSpan());
return Parse(utf8Bytes.AsMemory(0, length), readerOptions, utf8Bytes);
}
catch
{
// Holds document content, clear it before returning it.
utf8Bytes.AsSpan(0, length).Clear();
ArrayPool<byte>.Shared.Return(utf8Bytes);
throw;
}
}
/// <summary>
/// Parse a <see cref="Stream"/> as UTF-8 encoded data representing a single JSON value into a
/// JsonDocument. The Stream will be read to completion.
/// </summary>
/// <param name="utf8Json">JSON data to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <returns>
/// A JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="utf8Json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static JsonDocument Parse(Stream utf8Json, JsonDocumentOptions options = default)
{
if (utf8Json is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(utf8Json));
}
ArraySegment<byte> drained = ReadToEnd(utf8Json);
Debug.Assert(drained.Array != null);
try
{
return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array);
}
catch
{
// Holds document content, clear it before returning it.
drained.AsSpan().Clear();
ArrayPool<byte>.Shared.Return(drained.Array);
throw;
}
}
internal static JsonDocument ParseRented(PooledByteBufferWriter utf8Json, JsonDocumentOptions options = default)
{
return Parse(
utf8Json.WrittenMemory,
options.GetReaderOptions(),
extraRentedArrayPoolBytes: null,
extraPooledByteBufferWriter: utf8Json);
}
internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions options)
{
Debug.Assert(utf8Json != null);
ArraySegment<byte> drained = ReadToEnd(utf8Json);
Debug.Assert(drained.Array != null);
byte[] owned = new byte[drained.Count];
Buffer.BlockCopy(drained.Array, 0, owned, 0, drained.Count);
// Holds document content, clear it before returning it.
drained.AsSpan().Clear();
ArrayPool<byte>.Shared.Return(drained.Array);
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
}
internal static JsonDocument ParseValue(ReadOnlySpan<byte> utf8Json, JsonDocumentOptions options)
{
byte[] owned = new byte[utf8Json.Length];
utf8Json.CopyTo(owned);
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
}
internal static JsonDocument ParseValue(string json, JsonDocumentOptions options)
{
Debug.Assert(json != null);
return ParseValue(json.AsMemory(), options);
}
/// <summary>
/// Parse a <see cref="Stream"/> as UTF-8 encoded data representing a single JSON value into a
/// JsonDocument. The Stream will be read to completion.
/// </summary>
/// <param name="utf8Json">JSON data to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>
/// A Task to produce a JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="utf8Json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static Task<JsonDocument> ParseAsync(
Stream utf8Json,
JsonDocumentOptions options = default,
CancellationToken cancellationToken = default)
{
if (utf8Json is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(utf8Json));
}
return ParseAsyncCore(utf8Json, options, cancellationToken);
}
private static async Task<JsonDocument> ParseAsyncCore(
Stream utf8Json,
JsonDocumentOptions options = default,
CancellationToken cancellationToken = default)
{
ArraySegment<byte> drained = await ReadToEndAsync(utf8Json, cancellationToken).ConfigureAwait(false);
Debug.Assert(drained.Array != null);
try
{
return Parse(drained.AsMemory(), options.GetReaderOptions(), drained.Array);
}
catch
{
// Holds document content, clear it before returning it.
drained.AsSpan().Clear();
ArrayPool<byte>.Shared.Return(drained.Array);
throw;
}
}
internal static async Task<JsonDocument> ParseAsyncCoreUnrented(
Stream utf8Json,
JsonDocumentOptions options = default,
CancellationToken cancellationToken = default)
{
ArraySegment<byte> drained = await ReadToEndAsync(utf8Json, cancellationToken).ConfigureAwait(false);
Debug.Assert(drained.Array != null);
byte[] owned = new byte[drained.Count];
Buffer.BlockCopy(drained.Array, 0, owned, 0, drained.Count);
// Holds document content, clear it before returning it.
drained.AsSpan().Clear();
ArrayPool<byte>.Shared.Return(drained.Array);
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
}
/// <summary>
/// Parses text representing a single JSON value into a JsonDocument.
/// </summary>
/// <remarks>
/// The <see cref="ReadOnlyMemory{T}"/> value may be used for the entire lifetime of the
/// JsonDocument object, and the caller must ensure that the data therein does not change during
/// the object lifetime.
/// </remarks>
/// <param name="json">JSON text to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <returns>
/// A JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static JsonDocument Parse([StringSyntax(StringSyntaxAttribute.Json)] ReadOnlyMemory<char> json, JsonDocumentOptions options = default)
{
ReadOnlySpan<char> jsonChars = json.Span;
int expectedByteCount = JsonReaderHelper.GetUtf8ByteCount(jsonChars);
byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(expectedByteCount);
try
{
int actualByteCount = JsonReaderHelper.GetUtf8FromText(jsonChars, utf8Bytes);
Debug.Assert(expectedByteCount == actualByteCount);
return Parse(
utf8Bytes.AsMemory(0, actualByteCount),
options.GetReaderOptions(),
utf8Bytes);
}
catch
{
// Holds document content, clear it before returning it.
utf8Bytes.AsSpan(0, expectedByteCount).Clear();
ArrayPool<byte>.Shared.Return(utf8Bytes);
throw;
}
}
internal static JsonDocument ParseValue(ReadOnlyMemory<char> json, JsonDocumentOptions options)
{
ReadOnlySpan<char> jsonChars = json.Span;
int expectedByteCount = JsonReaderHelper.GetUtf8ByteCount(jsonChars);
byte[] owned;
byte[] utf8Bytes = ArrayPool<byte>.Shared.Rent(expectedByteCount);
try
{
int actualByteCount = JsonReaderHelper.GetUtf8FromText(jsonChars, utf8Bytes);
Debug.Assert(expectedByteCount == actualByteCount);
owned = new byte[actualByteCount];
Buffer.BlockCopy(utf8Bytes, 0, owned, 0, actualByteCount);
}
finally
{
// Holds document content, clear it before returning it.
utf8Bytes.AsSpan(0, expectedByteCount).Clear();
ArrayPool<byte>.Shared.Return(utf8Bytes);
}
return ParseUnrented(owned.AsMemory(), options.GetReaderOptions());
}
/// <summary>
/// Parses text representing a single JSON value into a JsonDocument.
/// </summary>
/// <param name="json">JSON text to parse.</param>
/// <param name="options">Options to control the reader behavior during parsing.</param>
/// <returns>
/// A JsonDocument representation of the JSON value.
/// </returns>
/// <exception cref="JsonException">
/// <paramref name="json"/> does not represent a valid single JSON value.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="options"/> contains unsupported options.
/// </exception>
public static JsonDocument Parse([StringSyntax(StringSyntaxAttribute.Json)] string json, JsonDocumentOptions options = default)
{
if (json is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(json));
}
return Parse(json.AsMemory(), options);
}
/// <summary>
/// Attempts to parse one JSON value (including objects or arrays) from the provided reader.
/// </summary>
/// <param name="reader">The reader to read.</param>
/// <param name="document">Receives the parsed document.</param>
/// <returns>
/// <see langword="true"/> if a value was read and parsed into a JsonDocument,
/// <see langword="false"/> if the reader ran out of data while parsing.
/// All other situations result in an exception being thrown.
/// </returns>
/// <remarks>
/// <para>
/// If the <see cref="Utf8JsonReader.TokenType"/> property of <paramref name="reader"/>
/// is <see cref="JsonTokenType.PropertyName"/> or <see cref="JsonTokenType.None"/>, the
/// reader will be advanced by one call to <see cref="Utf8JsonReader.Read"/> to determine
/// the start of the value.
/// </para>
///
/// <para>
/// Upon completion of this method, <paramref name="reader"/> will be positioned at the
/// final token in the JSON value. If an exception is thrown, or <see langword="false"/>
/// is returned, the reader is reset to the state it was in when the method was called.
/// </para>
///
/// <para>
/// This method makes a copy of the data the reader acted on, so there is no caller
/// requirement to maintain data integrity beyond the return of this method.
/// </para>
/// </remarks>
/// <exception cref="ArgumentException">
/// <paramref name="reader"/> is using unsupported options.
/// </exception>
/// <exception cref="ArgumentException">
/// The current <paramref name="reader"/> token does not start or represent a value.
/// </exception>
/// <exception cref="JsonException">
/// A value could not be read from the reader.
/// </exception>
public static bool TryParseValue(ref Utf8JsonReader reader, [NotNullWhen(true)] out JsonDocument? document)
{
return TryParseValue(ref reader, out document, shouldThrow: false, useArrayPools: true);
}
/// <summary>
/// Parses one JSON value (including objects or arrays) from the provided reader.
/// </summary>
/// <param name="reader">The reader to read.</param>
/// <returns>
/// A JsonDocument representing the value (and nested values) read from the reader.
/// </returns>
/// <remarks>
/// <para>
/// If the <see cref="Utf8JsonReader.TokenType"/> property of <paramref name="reader"/>
/// is <see cref="JsonTokenType.PropertyName"/> or <see cref="JsonTokenType.None"/>, the
/// reader will be advanced by one call to <see cref="Utf8JsonReader.Read"/> to determine
/// the start of the value.
/// </para>
///
/// <para>
/// Upon completion of this method, <paramref name="reader"/> will be positioned at the
/// final token in the JSON value. If an exception is thrown, the reader is reset to
/// the state it was in when the method was called.
/// </para>
///
/// <para>
/// This method makes a copy of the data the reader acted on, so there is no caller
/// requirement to maintain data integrity beyond the return of this method.
/// </para>
/// </remarks>
/// <exception cref="ArgumentException">
/// <paramref name="reader"/> is using unsupported options.
/// </exception>
/// <exception cref="ArgumentException">
/// The current <paramref name="reader"/> token does not start or represent a value.
/// </exception>
/// <exception cref="JsonException">
/// A value could not be read from the reader.
/// </exception>
public static JsonDocument ParseValue(ref Utf8JsonReader reader)
{
bool ret = TryParseValue(ref reader, out JsonDocument? document, shouldThrow: true, useArrayPools: true);
Debug.Assert(ret, "TryParseValue returned false with shouldThrow: true.");
Debug.Assert(document != null, "null document returned with shouldThrow: true.");
return document;
}
internal static bool TryParseValue(
ref Utf8JsonReader reader,
[NotNullWhen(true)] out JsonDocument? document,
bool shouldThrow,
bool useArrayPools)
{
JsonReaderState state = reader.CurrentState;
CheckSupportedOptions(state.Options, nameof(reader));
// Value copy to overwrite the ref on an exception and undo the destructive reads.
Utf8JsonReader restore = reader;
ReadOnlySpan<byte> valueSpan = default;
ReadOnlySequence<byte> valueSequence = default;
try
{
switch (reader.TokenType)
{
// A new reader was created and has never been read,
// so we need to move to the first token.
// (or a reader has terminated and we're about to throw)
case JsonTokenType.None:
// Using a reader loop the caller has identified a property they wish to
// hydrate into a JsonDocument. Move to the value first.
case JsonTokenType.PropertyName:
{
if (!reader.Read())
{
if (shouldThrow)
{
ThrowHelper.ThrowJsonReaderException(
ref reader,
ExceptionResource.ExpectedJsonTokens);
}
reader = restore;
document = null;
return false;
}
break;
}
}
switch (reader.TokenType)
{
// Any of the "value start" states are acceptable.
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
{
long startingOffset = reader.TokenStartIndex;
if (!reader.TrySkip())
{
if (shouldThrow)
{
ThrowHelper.ThrowJsonReaderException(
ref reader,
ExceptionResource.ExpectedJsonTokens);
}
reader = restore;
document = null;
return false;
}
long totalLength = reader.BytesConsumed - startingOffset;
ReadOnlySequence<byte> sequence = reader.OriginalSequence;
if (sequence.IsEmpty)
{
valueSpan = reader.OriginalSpan.Slice(
checked((int)startingOffset),
checked((int)totalLength));
}
else
{
valueSequence = sequence.Slice(startingOffset, totalLength);
}
Debug.Assert(
reader.TokenType == JsonTokenType.EndObject ||
reader.TokenType == JsonTokenType.EndArray);
break;
}
case JsonTokenType.False:
case JsonTokenType.True:
case JsonTokenType.Null:
if (useArrayPools)
{
if (reader.HasValueSequence)
{
valueSequence = reader.ValueSequence;
}
else
{
valueSpan = reader.ValueSpan;
}
break;
}
document = CreateForLiteral(reader.TokenType);
return true;
case JsonTokenType.Number:
{
if (reader.HasValueSequence)
{
valueSequence = reader.ValueSequence;
}
else
{
valueSpan = reader.ValueSpan;
}
break;
}
// String's ValueSequence/ValueSpan omits the quotes, we need them back.
case JsonTokenType.String:
{
ReadOnlySequence<byte> sequence = reader.OriginalSequence;
if (sequence.IsEmpty)
{
// Since the quoted string fit in a ReadOnlySpan originally
// the contents length plus the two quotes can't overflow.
int payloadLength = reader.ValueSpan.Length + 2;
Debug.Assert(payloadLength > 1);
ReadOnlySpan<byte> readerSpan = reader.OriginalSpan;
Debug.Assert(
readerSpan[(int)reader.TokenStartIndex] == (byte)'"',
$"Calculated span starts with {readerSpan[(int)reader.TokenStartIndex]}");
Debug.Assert(
readerSpan[(int)reader.TokenStartIndex + payloadLength - 1] == (byte)'"',
$"Calculated span ends with {readerSpan[(int)reader.TokenStartIndex + payloadLength - 1]}");
valueSpan = readerSpan.Slice((int)reader.TokenStartIndex, payloadLength);
}
else
{
long payloadLength = 2;
if (reader.HasValueSequence)
{
payloadLength += reader.ValueSequence.Length;
}
else
{
payloadLength += reader.ValueSpan.Length;
}
valueSequence = sequence.Slice(reader.TokenStartIndex, payloadLength);
Debug.Assert(
valueSequence.First.Span[0] == (byte)'"',
$"Calculated sequence starts with {valueSequence.First.Span[0]}");
Debug.Assert(
valueSequence.ToArray()[payloadLength - 1] == (byte)'"',
$"Calculated sequence ends with {valueSequence.ToArray()[payloadLength - 1]}");
}
break;
}
default:
{
if (shouldThrow)
{
// Default case would only hit if TokenType equals JsonTokenType.EndObject or JsonTokenType.EndArray in which case it would never be sequence
Debug.Assert(!reader.HasValueSequence);
byte displayByte = reader.ValueSpan[0];
ThrowHelper.ThrowJsonReaderException(
ref reader,
ExceptionResource.ExpectedStartOfValueNotFound,
displayByte);
}
reader = restore;
document = null;
return false;
}
}
}
catch
{
reader = restore;
throw;
}
int length = valueSpan.IsEmpty ? checked((int)valueSequence.Length) : valueSpan.Length;
if (useArrayPools)
{
byte[] rented = ArrayPool<byte>.Shared.Rent(length);
Span<byte> rentedSpan = rented.AsSpan(0, length);
try
{
if (valueSpan.IsEmpty)
{
valueSequence.CopyTo(rentedSpan);
}
else
{
valueSpan.CopyTo(rentedSpan);
}
document = Parse(rented.AsMemory(0, length), state.Options, rented);
}
catch
{
// This really shouldn't happen since the document was already checked
// for consistency by Skip. But if data mutations happened just after
// the calls to Read then the copy may not be valid.
rentedSpan.Clear();
ArrayPool<byte>.Shared.Return(rented);
throw;
}
}
else
{
byte[] owned;
if (valueSpan.IsEmpty)
{
owned = valueSequence.ToArray();
}
else
{
owned = valueSpan.ToArray();
}
document = ParseUnrented(owned, state.Options, reader.TokenType);
}
return true;
}
private static JsonDocument CreateForLiteral(JsonTokenType tokenType)
{
switch (tokenType)
{
case JsonTokenType.False:
s_falseLiteral ??= Create(JsonConstants.FalseValue.ToArray());
return s_falseLiteral;
case JsonTokenType.True:
s_trueLiteral ??= Create(JsonConstants.TrueValue.ToArray());
return s_trueLiteral;
default:
Debug.Assert(tokenType == JsonTokenType.Null);
s_nullLiteral ??= Create(JsonConstants.NullValue.ToArray());
return s_nullLiteral;
}
JsonDocument Create(byte[] utf8Json)
{
MetadataDb database = MetadataDb.CreateLocked(utf8Json.Length);
database.Append(tokenType, startLocation: 0, utf8Json.Length);
return new JsonDocument(utf8Json, database, isDisposable: false);
}
}
private static JsonDocument Parse(
ReadOnlyMemory<byte> utf8Json,
JsonReaderOptions readerOptions,
byte[]? extraRentedArrayPoolBytes = null,
PooledByteBufferWriter? extraPooledByteBufferWriter = null)
{
ReadOnlySpan<byte> utf8JsonSpan = utf8Json.Span;
var database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: false);
var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size);
try
{
Parse(utf8JsonSpan, readerOptions, ref database, ref stack);
}
catch
{
database.Dispose();
throw;
}
finally
{
stack.Dispose();
}
return new JsonDocument(utf8Json, database, extraRentedArrayPoolBytes, extraPooledByteBufferWriter);
}
private static JsonDocument ParseUnrented(
ReadOnlyMemory<byte> utf8Json,
JsonReaderOptions readerOptions,
JsonTokenType tokenType = JsonTokenType.None)
{
// These tokens should already have been processed.
Debug.Assert(
tokenType != JsonTokenType.Null &&
tokenType != JsonTokenType.False &&
tokenType != JsonTokenType.True);
ReadOnlySpan<byte> utf8JsonSpan = utf8Json.Span;
MetadataDb database;
if (tokenType == JsonTokenType.String || tokenType == JsonTokenType.Number)
{
// For primitive types, we can avoid renting MetadataDb and creating StackRowStack.
database = MetadataDb.CreateLocked(utf8Json.Length);
StackRowStack stack = default;
Parse(utf8JsonSpan, readerOptions, ref database, ref stack);
}
else
{
database = MetadataDb.CreateRented(utf8Json.Length, convertToAlloc: true);
var stack = new StackRowStack(JsonDocumentOptions.DefaultMaxDepth * StackRow.Size);
try
{
Parse(utf8JsonSpan, readerOptions, ref database, ref stack);
}
finally
{
stack.Dispose();
}
}
return new JsonDocument(utf8Json, database, isDisposable: false);
}
private static ArraySegment<byte> ReadToEnd(Stream stream)
{
int written = 0;
byte[]? rented = null;
ReadOnlySpan<byte> utf8Bom = JsonConstants.Utf8Bom;
try
{
if (stream.CanSeek)
{
// Ask for 1 more than the length to avoid resizing later,
// which is unnecessary in the common case where the stream length doesn't change.
long expectedLength = Math.Max(utf8Bom.Length, stream.Length - stream.Position) + 1;
rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
}
else
{
rented = ArrayPool<byte>.Shared.Rent(UnseekableStreamInitialRentSize);
}
int lastRead;
// Read up to 3 bytes to see if it's the UTF-8 BOM
do
{
// No need for checking for growth, the minimal rent sizes both guarantee it'll fit.
Debug.Assert(rented.Length >= utf8Bom.Length);
lastRead = stream.Read(
rented,
written,
utf8Bom.Length - written);
written += lastRead;
} while (lastRead > 0 && written < utf8Bom.Length);
// If we have 3 bytes, and they're the BOM, reset the write position to 0.
if (written == utf8Bom.Length &&
utf8Bom.SequenceEqual(rented.AsSpan(0, utf8Bom.Length)))
{
written = 0;
}
do
{
if (rented.Length == written)
{
byte[] toReturn = rented;
rented = ArrayPool<byte>.Shared.Rent(checked(toReturn.Length * 2));
Buffer.BlockCopy(toReturn, 0, rented, 0, toReturn.Length);
// Holds document content, clear it.
ArrayPool<byte>.Shared.Return(toReturn, clearArray: true);
}
lastRead = stream.Read(rented, written, rented.Length - written);
written += lastRead;
} while (lastRead > 0);
return new ArraySegment<byte>(rented, 0, written);
}
catch
{
if (rented != null)
{
// Holds document content, clear it before returning it.
rented.AsSpan(0, written).Clear();
ArrayPool<byte>.Shared.Return(rented);
}
throw;
}
}
private static async
#if NET
ValueTask<ArraySegment<byte>>
#else
Task<ArraySegment<byte>>
#endif
ReadToEndAsync(
Stream stream,
CancellationToken cancellationToken)
{
int written = 0;
byte[]? rented = null;
try
{
// Save the length to a local to be reused across awaits.
int utf8BomLength = JsonConstants.Utf8Bom.Length;
if (stream.CanSeek)
{
// Ask for 1 more than the length to avoid resizing later,
// which is unnecessary in the common case where the stream length doesn't change.
long expectedLength = Math.Max(utf8BomLength, stream.Length - stream.Position) + 1;
rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
}
else
{
rented = ArrayPool<byte>.Shared.Rent(UnseekableStreamInitialRentSize);
}
int lastRead;
// Read up to 3 bytes to see if it's the UTF-8 BOM
do
{
// No need for checking for growth, the minimal rent sizes both guarantee it'll fit.
Debug.Assert(rented.Length >= JsonConstants.Utf8Bom.Length);
lastRead = await stream.ReadAsync(
#if NET
rented.AsMemory(written, utf8BomLength - written),
#else
rented,
written,
utf8BomLength - written,
#endif
cancellationToken).ConfigureAwait(false);
written += lastRead;
} while (lastRead > 0 && written < utf8BomLength);
// If we have 3 bytes, and they're the BOM, reset the write position to 0.
if (written == utf8BomLength &&
JsonConstants.Utf8Bom.SequenceEqual(rented.AsSpan(0, utf8BomLength)))
{
written = 0;
}
do
{
if (rented.Length == written)
{
byte[] toReturn = rented;
rented = ArrayPool<byte>.Shared.Rent(toReturn.Length * 2);
Buffer.BlockCopy(toReturn, 0, rented, 0, toReturn.Length);
// Holds document content, clear it.
ArrayPool<byte>.Shared.Return(toReturn, clearArray: true);
}
lastRead = await stream.ReadAsync(
#if NET
rented.AsMemory(written),
#else
rented,
written,
rented.Length - written,
#endif
cancellationToken).ConfigureAwait(false);
written += lastRead;
} while (lastRead > 0);
return new ArraySegment<byte>(rented, 0, written);
}
catch
{
if (rented != null)
{
// Holds document content, clear it before returning it.
rented.AsSpan(0, written).Clear();
ArrayPool<byte>.Shared.Return(rented);
}
throw;
}
}
}
}
|