|
@@ -34,6 +34,7 @@ using System;
|
|
|
using System.Buffers;
|
|
|
using System.Buffers.Binary;
|
|
|
using System.Collections.Generic;
|
|
|
+using System.Diagnostics;
|
|
|
using System.IO;
|
|
|
using System.Runtime.CompilerServices;
|
|
|
using System.Runtime.InteropServices;
|
|
@@ -49,6 +50,7 @@ namespace Google.Protobuf
|
|
|
[SecuritySafeCritical]
|
|
|
internal static class ParsingPrimitives
|
|
|
{
|
|
|
+ private const int StackallocThreshold = 256;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Reads a length for length-delimited data.
|
|
@@ -58,7 +60,6 @@ namespace Google.Protobuf
|
|
|
/// to make the calling code clearer.
|
|
|
/// </remarks>
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
-
|
|
|
public static int ParseLength(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state)
|
|
|
{
|
|
|
return (int)ParseRawVarint32(ref buffer, ref state);
|
|
@@ -437,14 +438,6 @@ namespace Google.Protobuf
|
|
|
throw InvalidProtocolBufferException.NegativeSize();
|
|
|
}
|
|
|
|
|
|
- if (state.totalBytesRetired + state.bufferPos + size > state.currentLimit)
|
|
|
- {
|
|
|
- // Read to the end of the stream (up to the current limit) anyway.
|
|
|
- SkipRawBytes(ref buffer, ref state, state.currentLimit - state.totalBytesRetired - state.bufferPos);
|
|
|
- // Then fail.
|
|
|
- throw InvalidProtocolBufferException.TruncatedMessage();
|
|
|
- }
|
|
|
-
|
|
|
if (size <= state.bufferSize - state.bufferPos)
|
|
|
{
|
|
|
// We have all the bytes we need already.
|
|
@@ -453,36 +446,22 @@ namespace Google.Protobuf
|
|
|
state.bufferPos += size;
|
|
|
return bytes;
|
|
|
}
|
|
|
- else if (size < buffer.Length || size < state.segmentedBufferHelper.TotalLength)
|
|
|
+
|
|
|
+ return ReadRawBytesSlow(ref buffer, ref state, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static byte[] ReadRawBytesSlow(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state, int size)
|
|
|
+ {
|
|
|
+ ValidateCurrentLimit(ref buffer, ref state, size);
|
|
|
+
|
|
|
+ if ((!state.segmentedBufferHelper.TotalLength.HasValue && size < buffer.Length) ||
|
|
|
+ IsDataAvailableInSource(ref state, size))
|
|
|
{
|
|
|
// Reading more bytes than are in the buffer, but not an excessive number
|
|
|
// of bytes. We can safely allocate the resulting array ahead of time.
|
|
|
|
|
|
- // First copy what we have.
|
|
|
byte[] bytes = new byte[size];
|
|
|
- var bytesSpan = new Span<byte>(bytes);
|
|
|
- int pos = state.bufferSize - state.bufferPos;
|
|
|
- buffer.Slice(state.bufferPos, pos).CopyTo(bytesSpan.Slice(0, pos));
|
|
|
- state.bufferPos = state.bufferSize;
|
|
|
-
|
|
|
- // We want to use RefillBuffer() and then copy from the buffer into our
|
|
|
- // byte array rather than reading directly into our byte array because
|
|
|
- // the input may be unbuffered.
|
|
|
- state.segmentedBufferHelper.RefillBuffer(ref buffer, ref state, true);
|
|
|
-
|
|
|
- while (size - pos > state.bufferSize)
|
|
|
- {
|
|
|
- buffer.Slice(0, state.bufferSize)
|
|
|
- .CopyTo(bytesSpan.Slice(pos, state.bufferSize));
|
|
|
- pos += state.bufferSize;
|
|
|
- state.bufferPos = state.bufferSize;
|
|
|
- state.segmentedBufferHelper.RefillBuffer(ref buffer, ref state, true);
|
|
|
- }
|
|
|
-
|
|
|
- buffer.Slice(0, size - pos)
|
|
|
- .CopyTo(bytesSpan.Slice(pos, size - pos));
|
|
|
- state.bufferPos = size - pos;
|
|
|
-
|
|
|
+ ReadRawBytesIntoSpan(ref buffer, ref state, size, bytes);
|
|
|
return bytes;
|
|
|
}
|
|
|
else
|
|
@@ -518,7 +497,7 @@ namespace Google.Protobuf
|
|
|
}
|
|
|
|
|
|
// OK, got everything. Now concatenate it all into one buffer.
|
|
|
- byte[] bytes = new byte[size];
|
|
|
+ byte[] bytes = new byte[size];
|
|
|
int newPos = 0;
|
|
|
foreach (byte[] chunk in chunks)
|
|
|
{
|
|
@@ -543,13 +522,7 @@ namespace Google.Protobuf
|
|
|
throw InvalidProtocolBufferException.NegativeSize();
|
|
|
}
|
|
|
|
|
|
- if (state.totalBytesRetired + state.bufferPos + size > state.currentLimit)
|
|
|
- {
|
|
|
- // Read to the end of the stream anyway.
|
|
|
- SkipRawBytes(ref buffer, ref state, state.currentLimit - state.totalBytesRetired - state.bufferPos);
|
|
|
- // Then fail.
|
|
|
- throw InvalidProtocolBufferException.TruncatedMessage();
|
|
|
- }
|
|
|
+ ValidateCurrentLimit(ref buffer, ref state, size);
|
|
|
|
|
|
if (size <= state.bufferSize - state.bufferPos)
|
|
|
{
|
|
@@ -619,7 +592,7 @@ namespace Google.Protobuf
|
|
|
}
|
|
|
|
|
|
#if GOOGLE_PROTOBUF_SUPPORT_FAST_STRING
|
|
|
- if (length <= state.bufferSize - state.bufferPos && length > 0)
|
|
|
+ if (length <= state.bufferSize - state.bufferPos)
|
|
|
{
|
|
|
// Fast path: all bytes to decode appear in the same span.
|
|
|
ReadOnlySpan<byte> data = buffer.Slice(state.bufferPos, length);
|
|
@@ -638,20 +611,76 @@ namespace Google.Protobuf
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
- var decoder = WritingPrimitives.Utf8Encoding.GetDecoder();
|
|
|
+ return ReadStringSlow(ref buffer, ref state, length);
|
|
|
+ }
|
|
|
|
|
|
- // TODO: even if GOOGLE_PROTOBUF_SUPPORT_FAST_STRING is not supported,
|
|
|
- // we could still create a string efficiently by using Utf8Encoding.GetString(byte[] bytes, int index, int count)
|
|
|
- // whenever the buffer is backed by a byte array (and avoid creating a new byte array), but the problem is
|
|
|
- // there is no way to get the underlying byte array from a span.
|
|
|
+ /// <summary>
|
|
|
+ /// Reads a string assuming that it is spread across multiple spans in a <see cref="ReadOnlySequence{T}"/>.
|
|
|
+ /// </summary>
|
|
|
+ private static string ReadStringSlow(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state, int length)
|
|
|
+ {
|
|
|
+ ValidateCurrentLimit(ref buffer, ref state, length);
|
|
|
+
|
|
|
+#if GOOGLE_PROTOBUF_SUPPORT_FAST_STRING
|
|
|
+ if (IsDataAvailable(ref state, length))
|
|
|
+ {
|
|
|
+ // Read string data into a temporary buffer, either stackalloc'ed or from ArrayPool
|
|
|
+ // Once all data is read then call Encoding.GetString on buffer and return to pool if needed.
|
|
|
|
|
|
- // TODO: in case the string spans multiple buffer segments, creating a char[] and decoding into it and then
|
|
|
- // creating a string from that array might be more efficient than creating a string from the copied bytes.
|
|
|
+ byte[] byteArray = null;
|
|
|
+ Span<byte> byteSpan = length <= StackallocThreshold ?
|
|
|
+ stackalloc byte[length] :
|
|
|
+ (byteArray = ArrayPool<byte>.Shared.Rent(length));
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ unsafe
|
|
|
+ {
|
|
|
+ fixed (byte* pByteSpan = &MemoryMarshal.GetReference(byteSpan))
|
|
|
+ {
|
|
|
+ // Compiler doesn't like that a potentially stackalloc'd Span<byte> is being used
|
|
|
+ // in a method with a "ref Span<byte> buffer" argument. If the stackalloc'd span was assigned
|
|
|
+ // to the ref argument then bad things would happen. We'll never do that so it is ok.
|
|
|
+ // Make compiler happy by passing a new span created from pointer.
|
|
|
+ var tempSpan = new Span<byte>(pByteSpan, byteSpan.Length);
|
|
|
+ ReadRawBytesIntoSpan(ref buffer, ref state, length, tempSpan);
|
|
|
+
|
|
|
+ return WritingPrimitives.Utf8Encoding.GetString(pByteSpan, length);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ if (byteArray != null)
|
|
|
+ {
|
|
|
+ ArrayPool<byte>.Shared.Return(byteArray);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+#endif
|
|
|
|
|
|
// Slow path: Build a byte array first then copy it.
|
|
|
+ // This will be called when reading from a Stream because we don't know the length of the stream,
|
|
|
+ // or there is not enough data in the sequence. If there is not enough data then ReadRawBytes will
|
|
|
+ // throw an exception.
|
|
|
return WritingPrimitives.Utf8Encoding.GetString(ReadRawBytes(ref buffer, ref state, length), 0, length);
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Validates that the specified size doesn't exceed the current limit. If it does then remaining bytes
|
|
|
+ /// are skipped and an error is thrown.
|
|
|
+ /// </summary>
|
|
|
+ private static void ValidateCurrentLimit(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state, int size)
|
|
|
+ {
|
|
|
+ if (state.totalBytesRetired + state.bufferPos + size > state.currentLimit)
|
|
|
+ {
|
|
|
+ // Read to the end of the stream (up to the current limit) anyway.
|
|
|
+ SkipRawBytes(ref buffer, ref state, state.currentLimit - state.totalBytesRetired - state.bufferPos);
|
|
|
+ // Then fail.
|
|
|
+ throw InvalidProtocolBufferException.TruncatedMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
[SecuritySafeCritical]
|
|
|
private static byte ReadRawByte(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state)
|
|
|
{
|
|
@@ -731,5 +760,56 @@ namespace Google.Protobuf
|
|
|
{
|
|
|
return (long)(n >> 1) ^ -(long)(n & 1);
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Checks whether there is known data available of the specified size remaining to parse.
|
|
|
+ /// When parsing from a Stream this can return false because we have no knowledge of the amount
|
|
|
+ /// of data remaining in the stream until it is read.
|
|
|
+ /// </summary>
|
|
|
+ public static bool IsDataAvailable(ref ParserInternalState state, int size)
|
|
|
+ {
|
|
|
+ // Data fits in remaining buffer
|
|
|
+ if (size <= state.bufferSize - state.bufferPos)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return IsDataAvailableInSource(ref state, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Checks whether there is known data available of the specified size remaining to parse
|
|
|
+ /// in the underlying data source.
|
|
|
+ /// When parsing from a Stream this will return false because we have no knowledge of the amount
|
|
|
+ /// of data remaining in the stream until it is read.
|
|
|
+ /// </summary>
|
|
|
+ private static bool IsDataAvailableInSource(ref ParserInternalState state, int size)
|
|
|
+ {
|
|
|
+ // Data fits in remaining source data.
|
|
|
+ // Note that this will never be true when reading from a stream as the total length is unknown.
|
|
|
+ return size <= state.segmentedBufferHelper.TotalLength - state.totalBytesRetired - state.bufferPos;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Read raw bytes of the specified length into a span. The amount of data available and the current limit should
|
|
|
+ /// be checked before calling this method.
|
|
|
+ /// </summary>
|
|
|
+ private static void ReadRawBytesIntoSpan(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state, int length, Span<byte> byteSpan)
|
|
|
+ {
|
|
|
+ int remainingByteLength = length;
|
|
|
+ while (remainingByteLength > 0)
|
|
|
+ {
|
|
|
+ if (state.bufferSize - state.bufferPos == 0)
|
|
|
+ {
|
|
|
+ state.segmentedBufferHelper.RefillBuffer(ref buffer, ref state, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ ReadOnlySpan<byte> unreadSpan = buffer.Slice(state.bufferPos, Math.Min(remainingByteLength, state.bufferSize - state.bufferPos));
|
|
|
+ unreadSpan.CopyTo(byteSpan.Slice(length - remainingByteLength));
|
|
|
+
|
|
|
+ remainingByteLength -= unreadSpan.Length;
|
|
|
+ state.bufferPos += unreadSpan.Length;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+}
|