// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO.Pipelines; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Xunit; using MemoryPool = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPool; using MemoryPoolBlock = Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure.MemoryPoolBlock; namespace Microsoft.AspNetCore.Server.KestrelTests { public class MemoryPoolIteratorTests : IDisposable { private readonly MemoryPool _pool; public MemoryPoolIteratorTests() { _pool = new MemoryPool(); } public void Dispose() { _pool.Dispose(); } [Fact] public void Put() { var blocks = new MemoryPoolBlock[4]; for (var i = 0; i < 4; ++i) { blocks[i] = _pool.Lease(); blocks[i].End += 16; for (var j = 0; j < blocks.Length; ++j) { blocks[i].Array[blocks[i].Start + j] = 0x00; } if (i != 0) { blocks[i - 1].Next = blocks[i]; } } // put FF at first block's head var head = blocks[0].GetIterator(); Assert.True(head.Put(0xFF)); // data is put at correct position Assert.Equal(0xFF, blocks[0].Array[blocks[0].Start]); Assert.Equal(0x00, blocks[0].Array[blocks[0].Start + 1]); // iterator is moved to next byte after put Assert.Equal(1, head.Index - blocks[0].Start); for (var i = 0; i < 14; ++i) { // move itr to the end of the block 0 head.Take(); } // write to the end of block 0 Assert.True(head.Put(0xFE)); Assert.Equal(0xFE, blocks[0].Array[blocks[0].End - 1]); Assert.Equal(0x00, blocks[1].Array[blocks[1].Start]); // put data across the block link Assert.True(head.Put(0xFD)); Assert.Equal(0xFD, blocks[1].Array[blocks[1].Start]); Assert.Equal(0x00, blocks[1].Array[blocks[1].Start + 1]); // paint every block head = blocks[0].GetIterator(); for (var i = 0; i < 64; ++i) { Assert.True(head.Put((byte)i), $"Fail to put data at {i}."); } // Can't put anything by the end Assert.ThrowsAny(() => head.Put(0xFF)); for (var i = 0; i < 4; ++i) { _pool.Return(blocks[i]); } } [Fact] public async Task PeekArraySegment() { using (var pipeFactory = new PipeFactory()) { // Arrange var pipe = pipeFactory.Create(); var buffer = pipe.Writer.Alloc(); buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 })); await buffer.FlushAsync(); // Act var result = await pipe.Reader.PeekAsync(); // Assert Assert.Equal(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, result); pipe.Writer.Complete(); pipe.Reader.Complete(); } } [Fact] public async Task PeekArraySegmentAtEndOfDataReturnsDefaultArraySegment() { using (var pipeFactory = new PipeFactory()) { // Arrange var pipe = pipeFactory.Create(); pipe.Writer.Complete(); // Act var result = await pipe.Reader.PeekAsync(); // Assert // Assert.Equals doesn't work since xunit tries to access the underlying array. Assert.True(default(ArraySegment).Equals(result)); pipe.Reader.Complete(); } } [Fact] public async Task PeekArraySegmentAtBlockBoundary() { using (var pipeFactory = new PipeFactory()) { var pipe = pipeFactory.Create(); var buffer = pipe.Writer.Alloc(); buffer.Append(ReadableBuffer.Create(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 })); buffer.Append(ReadableBuffer.Create(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 })); await buffer.FlushAsync(); // Act var result = await pipe.Reader.PeekAsync(); // Assert Assert.Equal(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }, result); // Act // Advance past the data in the first block var readResult = pipe.Reader.ReadAsync().GetAwaiter().GetResult(); pipe.Reader.Advance(readResult.Buffer.Move(readResult.Buffer.Start, 8)); result = await pipe.Reader.PeekAsync(); // Assert Assert.Equal(new byte[] { 8, 9, 10, 11, 12, 13, 14, 15 }, result); pipe.Writer.Complete(); pipe.Reader.Complete(); } } [Fact] public void EmptyIteratorBehaviourIsValid() { const byte byteCr = (byte)'\n'; var end = default(MemoryPoolIterator); Assert.True(default(MemoryPoolIterator).IsDefault); Assert.True(default(MemoryPoolIterator).IsEnd); default(MemoryPoolIterator).CopyFrom(default(ArraySegment)); default(MemoryPoolIterator).CopyFromAscii(""); Assert.ThrowsAny(() => default(MemoryPoolIterator).Put(byteCr)); Assert.ThrowsAny(() => default(MemoryPoolIterator).GetLength(end)); } [Theory] [InlineData("a", "a", 1)] [InlineData("ab", "a...", 1)] [InlineData("abcde", "abcde", 5)] [InlineData("abcde", "abcd...", 4)] [InlineData("abcde", "abcde", 6)] public void TestGetAsciiStringEscaped(string input, string expected, int maxChars) { // Arrange var buffer = new Span(Encoding.ASCII.GetBytes(input)); // Act var result = buffer.GetAsciiStringEscaped(maxChars); // Assert Assert.Equal(expected, result); } [Fact] public void CorrectContentLengthsOutput() { using (var pool = new MemoryPool()) { var block = pool.Lease(); try { for (var i = 0u; i <= 9u; i++) { block.Reset(); var iter = new MemoryPoolIterator(block); iter.CopyFromNumeric(i); Assert.Equal(block.Array[block.Start], (byte)(i + '0')); Assert.Equal(block.End, block.Start + 1); Assert.Equal(iter.Index, block.End); } for (var i = 10u; i <= 99u; i++) { block.Reset(); var iter = new MemoryPoolIterator(block); iter.CopyFromNumeric(i); Assert.Equal(block.Array[block.Start], (byte)((i / 10) + '0')); Assert.Equal(block.Array[block.Start + 1], (byte)((i % 10) + '0')); Assert.Equal(block.End, block.Start + 2); Assert.Equal(iter.Index, block.End); } for (var i = 100u; i <= 999u; i++) { block.Reset(); var iter = new MemoryPoolIterator(block); iter.CopyFromNumeric(i); Assert.Equal(block.Array[block.Start], (byte)((i / 100) + '0')); Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 100) / 10) + '0')); Assert.Equal(block.Array[block.Start + 2], (byte)((i % 10) + '0')); Assert.Equal(block.End, block.Start + 3); Assert.Equal(iter.Index, block.End); } for (var i = 1000u; i <= 9999u; i++) { block.Reset(); var iter = new MemoryPoolIterator(block); iter.CopyFromNumeric(i); Assert.Equal(block.Array[block.Start], (byte)((i / 1000) + '0')); Assert.Equal(block.Array[block.Start + 1], (byte)(((i % 1000) / 100) + '0')); Assert.Equal(block.Array[block.Start + 2], (byte)(((i % 100) / 10) + '0')); Assert.Equal(block.Array[block.Start + 3], (byte)((i % 10) + '0')); Assert.Equal(block.End, block.Start + 4); Assert.Equal(iter.Index, block.End); } { block.Reset(); var iter = new MemoryPoolIterator(block); iter.CopyFromNumeric(ulong.MaxValue); var outputBytes = Encoding.ASCII.GetBytes(ulong.MaxValue.ToString("0")); for (var i = 0; i < outputBytes.Length; i++) { Assert.Equal(block.Array[block.Start + i], outputBytes[i]); } Assert.Equal(block.End, block.Start + outputBytes.Length); Assert.Equal(iter.Index, block.End); } } finally { pool.Return(block); } } } public static IEnumerable SeekByteLimitData { get { var vectorSpan = Vector.Count; // string input, char seek, int limit, int expectedBytesScanned, int expectedReturnValue var data = new List(); // Non-vector inputs data.Add(new object[] { "hello, world", 'h', 12, 1, 'h' }); data.Add(new object[] { "hello, world", ' ', 12, 7, ' ' }); data.Add(new object[] { "hello, world", 'd', 12, 12, 'd' }); data.Add(new object[] { "hello, world", '!', 12, 12, -1 }); data.Add(new object[] { "hello, world", 'h', 13, 1, 'h' }); data.Add(new object[] { "hello, world", ' ', 13, 7, ' ' }); data.Add(new object[] { "hello, world", 'd', 13, 12, 'd' }); data.Add(new object[] { "hello, world", '!', 13, 12, -1 }); data.Add(new object[] { "hello, world", 'h', 5, 1, 'h' }); data.Add(new object[] { "hello, world", 'o', 5, 5, 'o' }); data.Add(new object[] { "hello, world", ',', 5, 5, -1 }); data.Add(new object[] { "hello, world", 'd', 5, 5, -1 }); data.Add(new object[] { "abba", 'a', 4, 1, 'a' }); data.Add(new object[] { "abba", 'b', 4, 2, 'b' }); // Vector inputs // Single vector, no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan), 'b', vectorSpan, vectorSpan, -1 }); // Two vectors, no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan * 2), 'b', vectorSpan * 2, vectorSpan * 2, -1 }); // Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', vectorSpan * 2 + vectorSpan / 2, vectorSpan * 2 + vectorSpan / 2, -1 }); // For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments... for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2) { // ...place the seek char at vector and input boundaries... for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1)) { var input = new StringBuilder(new string('a', length)); input[i] = 'b'; // ...and check with a seek byte limit before, at, and past the seek char position... for (var limitOffset = -1; limitOffset <= 1; limitOffset++) { var limit = (i + 1) + limitOffset; if (limit >= i + 1) { // ...that Seek() succeeds when the seek char is within that limit... data.Add(new object[] { input.ToString(), 'b', limit, i + 1, 'b' }); } else { // ...and fails when it's not. data.Add(new object[] { input.ToString(), 'b', limit, Math.Min(length, limit), -1 }); } } } } return data; } } public static IEnumerable SeekIteratorLimitData { get { var vectorSpan = Vector.Count; // string input, char seek, char limitAt, int expectedReturnValue var data = new List(); // Non-vector inputs data.Add(new object[] { "hello, world", 'h', 'd', 'h' }); data.Add(new object[] { "hello, world", ' ', 'd', ' ' }); data.Add(new object[] { "hello, world", 'd', 'd', 'd' }); data.Add(new object[] { "hello, world", '!', 'd', -1 }); data.Add(new object[] { "hello, world", 'h', 'w', 'h' }); data.Add(new object[] { "hello, world", 'o', 'w', 'o' }); data.Add(new object[] { "hello, world", 'r', 'w', -1 }); data.Add(new object[] { "hello, world", 'd', 'w', -1 }); // Vector inputs // Single vector, no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan), 'b', 'b', -1 }); // Two vectors, no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan * 2), 'b', 'b', -1 }); // Two vectors plus non vector length (thus hitting slow path too), no seek char in input, expect failure data.Add(new object[] { new string('a', vectorSpan * 2 + vectorSpan / 2), 'b', 'b', -1 }); // For each input length from 1/2 to 3 1/2 vector spans in 1/2 vector span increments... for (var length = vectorSpan / 2; length <= vectorSpan * 3 + vectorSpan / 2; length += vectorSpan / 2) { // ...place the seek char at vector and input boundaries... for (var i = Math.Min(vectorSpan - 1, length - 1); i < length; i += ((i + 1) % vectorSpan == 0) ? 1 : Math.Min(i + (vectorSpan - 1), length - 1)) { var input = new StringBuilder(new string('a', length)); input[i] = 'b'; // ...along with sentinel characters to seek the limit iterator to... input[i - 1] = 'A'; if (i < length - 1) input[i + 1] = 'B'; // ...and check that Seek() succeeds with a limit iterator at or past the seek char position... data.Add(new object[] { input.ToString(), 'b', 'b', 'b' }); if (i < length - 1) data.Add(new object[] { input.ToString(), 'b', 'B', 'b' }); // ...and fails with a limit iterator before the seek char position. data.Add(new object[] { input.ToString(), 'b', 'A', -1 }); } } return data; } } } }