diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs index 80297beec433..db8c9f8ce7fa 100644 --- a/src/Http/WebUtilities/src/MultipartReaderStream.cs +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -116,6 +116,11 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotSupportedException(); } + public override void Write(ReadOnlySpan buffer) + { + throw new NotSupportedException(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { throw new NotSupportedException(); @@ -164,6 +169,9 @@ private int UpdatePosition(int read) } public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) { if (_finished) { @@ -184,7 +192,7 @@ public override int Read(byte[] buffer, int offset, int count) if (index != 0) { // Sync, it's already buffered - var slice = buffer.AsSpan(offset, Math.Min(count, index)); + var slice = buffer[..Math.Min(buffer.Length, index)]; var readAmount = _innerStream.Read(slice); return UpdatePosition(readAmount); @@ -204,7 +212,7 @@ public override int Read(byte[] buffer, int offset, int count) // We found a possible match, return any data before it. if (matchOffset > bufferedData.Offset) { - read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + read = _innerStream.Read(buffer[..Math.Min(buffer.Length, matchOffset - bufferedData.Offset)]); return UpdatePosition(read); } @@ -215,7 +223,7 @@ public override int Read(byte[] buffer, int offset, int count) } // No possible boundary match within the buffered data, return the data from the buffer. - read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + read = _innerStream.Read(buffer[..Math.Min(buffer.Length, bufferedData.Count)]); return UpdatePosition(read); static int ReadBoundary(MultipartReaderStream stream, int length) diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs index de2396cbfde8..16258e40724d 100644 --- a/src/Http/WebUtilities/test/MultipartReaderTests.cs +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -93,6 +93,18 @@ public class MultipartReaderTests "\r\n" + "--9051914041544843365"; + private static string ReadToEndUsingSpans(Stream stream, int spanLength) + { + var buffer = new byte[spanLength]; + var result = new MemoryStream(); + int read; + while ((read = stream.Read(buffer.AsSpan())) > 0) + { + result.Write(buffer, 0, read); + } + return Encoding.ASCII.GetString(result.ToArray()); + } + private static MemoryStream MakeStream(string text) { return new MemoryStream(Encoding.UTF8.GetBytes(text)); @@ -502,6 +514,29 @@ public async Task SyncReadWithOffsetWorks() Assert.Null(await reader.ReadNextSectionAsync()); } + [Theory] + // The default buffer holds the whole body, so every read sees the full boundary. + [InlineData(4096)] + // A buffer smaller than the body forces partial boundary matches at the end of the buffered data. + [InlineData(64)] + public async Task MultipartReader_ReadSectionBodyIntoSpan_ReadsEachSection(int bufferSize) + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream, bufferSize); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text default", ReadToEndUsingSpans(section.Body, spanLength: 5)); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("Content of a.txt.\r\n", ReadToEndUsingSpans(section.Body, spanLength: 5)); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + [Fact] public async Task MultipartReader_BoundaryWithUnexpectedTrailingData_ThrowsIOException() {