Merge pull request #8473 from Jensaarai/ReadOnlySpan

C#: Add ParseFrom/MergeFrom ReadOnlySpan<byte>
diff --git a/csharp/src/Google.Protobuf.Test/CodedInputStreamTest.cs b/csharp/src/Google.Protobuf.Test/CodedInputStreamTest.cs
index 0ad286f..5e72525 100644
--- a/csharp/src/Google.Protobuf.Test/CodedInputStreamTest.cs
+++ b/csharp/src/Google.Protobuf.Test/CodedInputStreamTest.cs
@@ -161,12 +161,21 @@
 

         private static void AssertReadFromParseContext(ReadOnlySequence<byte> input, ParseContextAssertAction assertAction, bool assertIsAtEnd)

         {

+            // Check as ReadOnlySequence<byte>

             ParseContext.Initialize(input, out ParseContext parseCtx);

             assertAction(ref parseCtx);

             if (assertIsAtEnd)

             {

                 Assert.IsTrue(SegmentedBufferHelper.IsAtEnd(ref parseCtx.buffer, ref parseCtx.state));

             }

+

+            // Check as ReadOnlySpan<byte>

+            ParseContext.Initialize(input.ToArray().AsSpan(), out ParseContext spanParseContext);

+            assertAction(ref spanParseContext);

+            if (assertIsAtEnd)

+            {

+                Assert.IsTrue(SegmentedBufferHelper.IsAtEnd(ref spanParseContext.buffer, ref spanParseContext.state));

+            }

         }

 

         [Test]

diff --git a/csharp/src/Google.Protobuf.Test/MessageParsingHelpers.cs b/csharp/src/Google.Protobuf.Test/MessageParsingHelpers.cs
index 65d2fe0..05f1e36 100644
--- a/csharp/src/Google.Protobuf.Test/MessageParsingHelpers.cs
+++ b/csharp/src/Google.Protobuf.Test/MessageParsingHelpers.cs
@@ -41,32 +41,38 @@
     {
         public static void AssertReadingMessage<T>(MessageParser<T> parser, byte[] bytes, Action<T> assert) where T : IMessage<T>
         {
-            var parsedStream = parser.ParseFrom(bytes);
+            var parsedMsg = parser.ParseFrom(bytes);
+            assert(parsedMsg);
 
             // Load content as single segment
-            var parsedBuffer = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
-            assert(parsedBuffer);
+            parsedMsg = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
+            assert(parsedMsg);
 
             // Load content as multiple segments
-            parsedBuffer = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
-            assert(parsedBuffer);
+            parsedMsg = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
+            assert(parsedMsg);
 
-            assert(parsedStream);
+            // Load content as ReadOnlySpan
+            parsedMsg = parser.ParseFrom(new ReadOnlySpan<byte>(bytes));
+            assert(parsedMsg);
         }
 
         public static void AssertReadingMessage(MessageParser parser, byte[] bytes, Action<IMessage> assert)
         {
-            var parsedStream = parser.ParseFrom(bytes);
+            var parsedMsg = parser.ParseFrom(bytes);
+            assert(parsedMsg);
 
             // Load content as single segment
-            var parsedBuffer = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
-            assert(parsedBuffer);
+            parsedMsg = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
+            assert(parsedMsg);
 
             // Load content as multiple segments
-            parsedBuffer = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
-            assert(parsedBuffer);
+            parsedMsg = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
+            assert(parsedMsg);
 
-            assert(parsedStream);
+            // Load content as ReadOnlySpan
+            parsedMsg = parser.ParseFrom(new ReadOnlySpan<byte>(bytes));
+            assert(parsedMsg);
         }
 
         public static void AssertReadingMessageThrows<TMessage, TException>(MessageParser<TMessage> parser, byte[] bytes)
@@ -76,6 +82,8 @@
             Assert.Throws<TException>(() => parser.ParseFrom(bytes));
 
             Assert.Throws<TException>(() => parser.ParseFrom(new ReadOnlySequence<byte>(bytes)));
+
+            Assert.Throws<TException>(() => parser.ParseFrom(new ReadOnlySpan<byte>(bytes)));
         }
 
         public static void AssertRoundtrip<T>(MessageParser<T> parser, T message, Action<T> additionalAssert = null) where T : IMessage<T>
@@ -87,20 +95,24 @@
             message.WriteTo(bufferWriter);
             Assert.AreEqual(bytes, bufferWriter.WrittenSpan.ToArray(), "Both serialization approaches need to result in the same data.");
 
+            var parsedMsg = parser.ParseFrom(bytes);
+            Assert.AreEqual(message, parsedMsg);
+            additionalAssert?.Invoke(parsedMsg);
+
             // Load content as single segment
-            var parsedBuffer = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
-            Assert.AreEqual(message, parsedBuffer);
-            additionalAssert?.Invoke(parsedBuffer);
+            parsedMsg = parser.ParseFrom(new ReadOnlySequence<byte>(bytes));
+            Assert.AreEqual(message, parsedMsg);
+            additionalAssert?.Invoke(parsedMsg);
 
             // Load content as multiple segments
-            parsedBuffer = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
-            Assert.AreEqual(message, parsedBuffer);
-            additionalAssert?.Invoke(parsedBuffer);
+            parsedMsg = parser.ParseFrom(ReadOnlySequenceFactory.CreateWithContent(bytes));
+            Assert.AreEqual(message, parsedMsg);
+            additionalAssert?.Invoke(parsedMsg);
 
-            var parsedStream = parser.ParseFrom(bytes);
-
-            Assert.AreEqual(message, parsedStream);
-            additionalAssert?.Invoke(parsedStream);
+            // Load content as ReadOnlySpan
+            parsedMsg = parser.ParseFrom(new ReadOnlySpan<byte>(bytes));
+            Assert.AreEqual(message, parsedMsg);
+            additionalAssert?.Invoke(parsedMsg);
         }
 
         public static void AssertWritingMessage(IMessage message)
diff --git a/csharp/src/Google.Protobuf/CodedInputStream.cs b/csharp/src/Google.Protobuf/CodedInputStream.cs
index b09f96c..27b23c0 100644
--- a/csharp/src/Google.Protobuf/CodedInputStream.cs
+++ b/csharp/src/Google.Protobuf/CodedInputStream.cs
@@ -435,8 +435,7 @@
             // we will need to switch back again to CodedInputStream-based parsing (which involves copying and storing the state) to be able to

             // invoke the legacy MergeFrom(CodedInputStream) method.

             // For now, this inefficiency is fine, considering this is only a backward-compatibility scenario (and regenerating the code fixes it).

-            var span = new ReadOnlySpan<byte>(buffer);

-            ParseContext.Initialize(ref span, ref state, out ParseContext ctx);

+            ParseContext.Initialize(buffer.AsSpan(), ref state, out ParseContext ctx);

             try

             {

                 ParsingPrimitivesMessages.ReadMessage(ref ctx, builder);

diff --git a/csharp/src/Google.Protobuf/MessageExtensions.cs b/csharp/src/Google.Protobuf/MessageExtensions.cs
index 36a9df7..c4b3f82 100644
--- a/csharp/src/Google.Protobuf/MessageExtensions.cs
+++ b/csharp/src/Google.Protobuf/MessageExtensions.cs
@@ -80,6 +80,15 @@
             MergeFrom(message, input, false, null);
 
         /// <summary>
+        /// Merges data from the given span into an existing message.
+        /// </summary>
+        /// <param name="message">The message to merge the data into.</param>
+        /// <param name="span">Span containing the data to merge, which must be protobuf-encoded binary data.</param>
+        [SecuritySafeCritical]
+        public static void MergeFrom(this IMessage message, ReadOnlySpan<byte> span) =>
+            MergeFrom(message, span, false, null);
+
+        /// <summary>
         /// Merges length-delimited data from the given stream into an existing message.
         /// </summary>
         /// <remarks>
@@ -294,6 +303,16 @@
             ParsingPrimitivesMessages.CheckReadEndOfStreamTag(ref ctx.state);
         }
 
+        [SecuritySafeCritical]
+        internal static void MergeFrom(this IMessage message, ReadOnlySpan<byte> data, bool discardUnknownFields, ExtensionRegistry registry)
+        {
+            ParseContext.Initialize(data, out ParseContext ctx);
+            ctx.DiscardUnknownFields = discardUnknownFields;
+            ctx.ExtensionRegistry = registry;
+            ParsingPrimitivesMessages.ReadRawMessage(ref ctx, message);
+            ParsingPrimitivesMessages.CheckReadEndOfStreamTag(ref ctx.state);
+        }
+
         internal static void MergeDelimitedFrom(this IMessage message, Stream input, bool discardUnknownFields, ExtensionRegistry registry)
         {
             ProtoPreconditions.CheckNotNull(message, "message");
diff --git a/csharp/src/Google.Protobuf/MessageParser.cs b/csharp/src/Google.Protobuf/MessageParser.cs
index f8b26c2..30a25a8 100644
--- a/csharp/src/Google.Protobuf/MessageParser.cs
+++ b/csharp/src/Google.Protobuf/MessageParser.cs
@@ -129,6 +129,19 @@
         }
 
         /// <summary>
+        /// Parses a message from the given span.
+        /// </summary>
+        /// <param name="data">The data to parse.</param>
+        /// <returns>The parsed message.</returns>
+        [SecuritySafeCritical]
+        public IMessage ParseFrom(ReadOnlySpan<byte> data)
+        {
+            IMessage message = factory();
+            message.MergeFrom(data, DiscardUnknownFields, Extensions);
+            return message;
+        }
+
+        /// <summary>
         /// Parses a length-delimited message from the given stream.
         /// </summary>
         /// <remarks>
@@ -316,6 +329,19 @@
         }
 
         /// <summary>
+        /// Parses a message from the given span.
+        /// </summary>
+        /// <param name="data">The data to parse.</param>
+        /// <returns>The parsed message.</returns>
+        [SecuritySafeCritical]
+        public new T ParseFrom(ReadOnlySpan<byte> data)
+        {
+            T message = factory();
+            message.MergeFrom(data, DiscardUnknownFields, Extensions);
+            return message;
+        }
+
+        /// <summary>
         /// Parses a length-delimited message from the given stream.
         /// </summary>
         /// <remarks>
diff --git a/csharp/src/Google.Protobuf/ParseContext.cs b/csharp/src/Google.Protobuf/ParseContext.cs
index bf46236..7b278b5 100644
--- a/csharp/src/Google.Protobuf/ParseContext.cs
+++ b/csharp/src/Google.Protobuf/ParseContext.cs
@@ -58,8 +58,27 @@
         internal ReadOnlySpan<byte> buffer;
         internal ParserInternalState state;
 
+        /// <summary>
+        /// Initialize a <see cref="ParseContext"/>, building all <see cref="ParserInternalState"/> from defaults and
+        /// the given <paramref name="buffer"/>.
+        /// </summary>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        internal static void Initialize(ref ReadOnlySpan<byte> buffer, ref ParserInternalState state, out ParseContext ctx)
+        internal static void Initialize(ReadOnlySpan<byte> buffer, out ParseContext ctx)
+        {
+            ParserInternalState state = default;
+            state.sizeLimit = DefaultSizeLimit;
+            state.recursionLimit = DefaultRecursionLimit;
+            state.currentLimit = int.MaxValue;
+            state.bufferSize = buffer.Length;
+
+            Initialize(buffer, ref state, out ctx);
+        }
+
+        /// <summary>
+        /// Initialize a <see cref="ParseContext"/> using existing <see cref="ParserInternalState"/>, e.g. from <see cref="CodedInputStream"/>.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static void Initialize(ReadOnlySpan<byte> buffer, ref ParserInternalState state, out ParseContext ctx)
         {
             ctx.buffer = buffer;
             ctx.state = state;