diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
index 0d7b5ce6e5..79552ea944 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
@@ -420,8 +420,8 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
if (_responseStarted) return;
await FireOnStarting();
-
- if (_applicationException != null)
+
+ if (_applicationException != null)
{
throw new ObjectDisposedException(
"The response has been aborted due to an unhandled application exception.",
@@ -591,12 +591,17 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
scan.Take();
begin = scan;
- var chFound = scan.Seek(' ', '?');
- if (chFound == -1)
+
+ var needDecode = false;
+ var chFound = scan.Seek(' ', '?', '%');
+ if (chFound == '%')
{
- return false;
+ needDecode = true;
+ chFound = scan.Seek(' ', '?');
}
- var requestUri = begin.GetString(scan);
+
+ var pathBegin = begin;
+ var pathEnd = scan;
var queryString = "";
if (chFound == '?')
@@ -623,9 +628,16 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
return false;
}
+ if (needDecode)
+ {
+ pathEnd = UrlPathDecoder.Unescape(pathBegin, pathEnd);
+ }
+
+ var requestUrlPath = pathBegin.GetString(pathEnd);
+
consumed = scan;
Method = method;
- RequestUri = requestUri;
+ RequestUri = requestUrlPath;
QueryString = queryString;
HttpVersion = httpVersion;
Path = RequestUri;
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/UrlPathDecoder.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/UrlPathDecoder.cs
new file mode 100644
index 0000000000..66608a4a22
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Http/UrlPathDecoder.cs
@@ -0,0 +1,306 @@
+// 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 Microsoft.AspNet.Server.Kestrel.Infrastructure;
+
+namespace Microsoft.AspNet.Server.Kestrel.Http
+{
+ public class UrlPathDecoder
+ {
+ ///
+ /// Unescapes the string between given memory iterators in place.
+ ///
+ /// The iterator points to the beginning of the sequence.
+ /// The iterator points to the byte behind the end of the sequence.
+ /// The iterator points to the byte behind the end of the processed sequence.
+ public static MemoryPoolIterator2 Unescape(MemoryPoolIterator2 start, MemoryPoolIterator2 end)
+ {
+ // the slot to read the input
+ var reader = start;
+
+ // the slot to write the unescaped byte
+ var writer = reader;
+
+ while (true)
+ {
+ if (CompareIterators(ref reader, ref end))
+ {
+ return writer;
+ }
+
+ if (reader.Peek() == '%')
+ {
+ var decodeReader = reader;
+
+ // If decoding process succeeds, the writer iterator will be moved
+ // to the next write-ready location. On the other hand if the scanned
+ // percent-encodings cannot be interpreted as sequence of UTF-8 octets,
+ // these bytes should be copied to output as is.
+ // The decodeReader iterator is always moved to the first byte not yet
+ // be scanned after the process. A failed decoding means the chars
+ // between the reader and decodeReader can be copied to output untouched.
+ if (!DecodeCore(ref decodeReader, ref writer, end))
+ {
+ Copy(reader, decodeReader, ref writer);
+ }
+
+ reader = decodeReader;
+ }
+ else
+ {
+ writer.Put((byte)reader.Take());
+ }
+ }
+ }
+
+ ///
+ /// Unescape the percent-encodings
+ ///
+ /// The iterator point to the first % char
+ /// The place to write to
+ /// The end of the sequence
+ private static bool DecodeCore(ref MemoryPoolIterator2 reader, ref MemoryPoolIterator2 writer, MemoryPoolIterator2 end)
+ {
+ // preserves the original head. if the percent-encodings cannot be interpreted as sequence of UTF-8 octets,
+ // bytes from this till the last scanned one will be copied to the memory pointed by writer.
+ var byte1 = UnescapePercentEncoding(ref reader, end);
+ if (byte1 == -1)
+ {
+ return false;
+ }
+
+ if (byte1 <= 0x7F)
+ {
+ // first byte < U+007f, it is a single byte ASCII
+ writer.Put((byte)byte1);
+ return true;
+ }
+
+ int byte2 = 0, byte3 = 0, byte4 = 0;
+
+ // anticipate more bytes
+ var currentDecodeBits = 0;
+ var byteCount = 1;
+ var expectValueMin = 0;
+ if ((byte1 & 0xE0) == 0xC0)
+ {
+ // 110x xxxx, expect one more byte
+ currentDecodeBits = byte1 & 0x1F;
+ byteCount = 2;
+ expectValueMin = 0x80;
+ }
+ else if ((byte1 & 0xF0) == 0xE0)
+ {
+ // 1110 xxxx, expect two more bytes
+ currentDecodeBits = byte1 & 0x0F;
+ byteCount = 3;
+ expectValueMin = 0x800;
+ }
+ else if ((byte1 & 0xF8) == 0xF0)
+ {
+ // 1111 0xxx, expect three more bytes
+ currentDecodeBits = byte1 & 0x07;
+ byteCount = 4;
+ expectValueMin = 0x10000;
+ }
+ else
+ {
+ // invalid first byte
+ return false;
+ }
+
+ var remainingBytes = byteCount - 1;
+ while (remainingBytes > 0)
+ {
+ // read following three chars
+ if (CompareIterators(ref reader, ref end))
+ {
+ return false;
+ }
+
+ var nextItr = reader;
+ var nextByte = UnescapePercentEncoding(ref nextItr, end);
+ if (nextByte == -1)
+ {
+ return false;
+ }
+
+ if ((nextByte & 0xC0) != 0x80)
+ {
+ // the follow up byte is not in form of 10xx xxxx
+ return false;
+ }
+
+ currentDecodeBits = (currentDecodeBits << 6) | (nextByte & 0x3F);
+ remainingBytes--;
+
+ if (remainingBytes == 1 && currentDecodeBits >= 0x360 && currentDecodeBits <= 0x37F)
+ {
+ // this is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that
+ // are not allowed in UTF-8;
+ return false;
+ }
+
+ if (remainingBytes == 2 && currentDecodeBits >= 0x110)
+ {
+ // this is going to be out of the upper Unicode bound 0x10FFFF.
+ return false;
+ }
+
+ reader = nextItr;
+ if (byteCount - remainingBytes == 2)
+ {
+ byte2 = nextByte;
+ }
+ else if (byteCount - remainingBytes == 3)
+ {
+ byte3 = nextByte;
+ }
+ else if (byteCount - remainingBytes == 4)
+ {
+ byte4 = nextByte;
+ }
+ }
+
+ if (currentDecodeBits < expectValueMin)
+ {
+ // overlong encoding (e.g. using 2 bytes to encode something that only needed 1).
+ return false;
+ }
+
+ // all bytes are verified, write to the output
+ if (byteCount > 0)
+ {
+ writer.Put((byte)byte1);
+ }
+ if (byteCount > 1)
+ {
+ writer.Put((byte)byte2);
+ }
+ if (byteCount > 2)
+ {
+ writer.Put((byte)byte3);
+ }
+ if (byteCount > 3)
+ {
+ writer.Put((byte)byte4);
+ }
+
+ return true;
+ }
+
+ private static void Copy(MemoryPoolIterator2 head, MemoryPoolIterator2 tail, ref MemoryPoolIterator2 writer)
+ {
+ while (!CompareIterators(ref head, ref tail))
+ {
+ writer.Put((byte)head.Take());
+ }
+ }
+
+ ///
+ /// Read the percent-encoding and try unescape it.
+ ///
+ /// The operation first peek at the character the
+ /// iterator points at. If it is % the is then
+ /// moved on to scan the following to characters. If the two following
+ /// characters are hexadecimal literals they will be unescaped and the
+ /// value will be returned.
+ ///
+ /// If the first character is not % the iterator
+ /// will be removed beyond the location of % and -1 will be returned.
+ ///
+ /// If the following two characters can't be successfully unescaped the
+ /// iterator will be move behind the % and -1
+ /// will be returned.
+ ///
+ /// The value to read
+ /// The end of the sequence
+ /// The unescaped byte if success. Otherwise return -1.
+ private static int UnescapePercentEncoding(ref MemoryPoolIterator2 scan, MemoryPoolIterator2 end)
+ {
+ if (scan.Take() != '%')
+ {
+ return -1;
+ }
+
+ var probe = scan;
+
+ int value1 = ReadHex(ref probe, end);
+ if (value1 == -1)
+ {
+ return -1;
+ }
+
+ int value2 = ReadHex(ref probe, end);
+ if (value2 == -1)
+ {
+ return -1;
+ }
+
+ if (SkipUnescape(value1, value2))
+ {
+ return -1;
+ }
+
+ scan = probe;
+ return (value1 << 4) + value2;
+ }
+
+ ///
+ /// Read the next char and convert it into hexadecimal value.
+ ///
+ /// The iterator will be moved to the next
+ /// byte no matter no matter whether the operation successes.
+ ///
+ /// The value to read
+ /// The end of the sequence
+ /// The hexadecimal value if successes, otherwise -1.
+ private static int ReadHex(ref MemoryPoolIterator2 scan, MemoryPoolIterator2 end)
+ {
+ if (CompareIterators(ref scan, ref end))
+ {
+ return -1;
+ }
+
+ var value = scan.Take();
+ var isHead = (((value >= '0') && (value <= '9')) ||
+ ((value >= 'A') && (value <= 'F')) ||
+ ((value >= 'a') && (value <= 'f')));
+
+ if (!isHead)
+ {
+ return -1;
+ }
+
+ if (value <= '9')
+ {
+ return value - '0';
+ }
+ else if (value <= 'F')
+ {
+ return (value - 'A') + 10;
+ }
+ else // a - f
+ {
+ return (value - 'a') + 10;
+ }
+ }
+
+ private static bool SkipUnescape(int value1, int value2)
+ {
+ // skip %2F
+ if (value1 == 2 && value2 == 15)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool CompareIterators(ref MemoryPoolIterator2 lhs, ref MemoryPoolIterator2 rhs)
+ {
+ // uses ref parameter to save cost of copying
+ return (lhs.Block == rhs.Block) && (lhs.Index == rhs.Index);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2.cs b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2.cs
index 8964093d03..7de5c7fb10 100644
--- a/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel/Infrastructure/MemoryPoolIterator2.cs
@@ -1,4 +1,7 @@
-using System;
+// 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.Linq;
using System.Numerics;
using System.Text;
@@ -9,9 +12,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
{
///
/// Array of "minus one" bytes of the length of SIMD operations on the current hardware. Used as an argument in the
- /// vector dot product that counts matching character occurence.
+ /// vector dot product that counts matching character occurrence.
///
- private static Vector _dotCount = new Vector(Byte.MaxValue);
+ private static Vector _dotCount = new Vector(Byte.MaxValue);
///
/// Array of negative numbers starting at 0 and continuing for the length of SIMD operations on the current hardware.
@@ -295,6 +298,167 @@ namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
}
}
+ public int Seek(int char0, int char1, int char2)
+ {
+ if (IsDefault)
+ {
+ return -1;
+ }
+
+ var byte0 = (byte)char0;
+ var byte1 = (byte)char1;
+ var byte2 = (byte)char2;
+ var vectorStride = Vector.Count;
+ var ch0Vector = new Vector(byte0);
+ var ch1Vector = new Vector(byte1);
+ var ch2Vector = new Vector(byte2);
+
+ var block = _block;
+ var index = _index;
+ var array = block.Array;
+ while (true)
+ {
+ while (block.End == index)
+ {
+ if (block.Next == null)
+ {
+ _block = block;
+ _index = index;
+ return -1;
+ }
+ block = block.Next;
+ index = block.Start;
+ array = block.Array;
+ }
+ while (block.End != index)
+ {
+ var following = block.End - index;
+ if (following >= vectorStride)
+ {
+ var data = new Vector(array, index);
+ var ch0Equals = Vector.Equals(data, ch0Vector);
+ var ch0Count = Vector.Dot(ch0Equals, _dotCount);
+ var ch1Equals = Vector.Equals(data, ch1Vector);
+ var ch1Count = Vector.Dot(ch1Equals, _dotCount);
+ var ch2Equals = Vector.Equals(data, ch2Vector);
+ var ch2Count = Vector.Dot(ch2Equals, _dotCount);
+
+ if (ch0Count == 0 && ch1Count == 0 && ch2Count == 0)
+ {
+ index += vectorStride;
+ continue;
+ }
+ else if (ch0Count < 2 && ch1Count < 2 && ch2Count < 2)
+ {
+ var ch0Index = ch0Count == 1 ? Vector.Dot(ch0Equals, _dotIndex) : byte.MaxValue;
+ var ch1Index = ch1Count == 1 ? Vector.Dot(ch1Equals, _dotIndex) : byte.MaxValue;
+ var ch2Index = ch2Count == 1 ? Vector.Dot(ch2Equals, _dotIndex) : byte.MaxValue;
+
+ int toReturn, toMove;
+ if (ch0Index < ch1Index)
+ {
+ if (ch0Index < ch2Index)
+ {
+ toReturn = char0;
+ toMove = ch0Index;
+ }
+ else
+ {
+ toReturn = char2;
+ toMove = ch2Index;
+ }
+ }
+ else
+ {
+ if (ch1Index < ch2Index)
+ {
+ toReturn = char1;
+ toMove = ch1Index;
+ }
+ else
+ {
+ toReturn = char2;
+ toMove = ch2Index;
+ }
+ }
+
+ _block = block;
+ _index = index + toMove;
+ return toReturn;
+ }
+ else
+ {
+ following = vectorStride;
+ }
+ }
+ while (following > 0)
+ {
+ var byteIndex = block.Array[index];
+ if (byteIndex == byte0)
+ {
+ _block = block;
+ _index = index;
+ return char0;
+ }
+ else if (byteIndex == byte1)
+ {
+ _block = block;
+ _index = index;
+ return char1;
+ }
+ else if (byteIndex == byte2)
+ {
+ _block = block;
+ _index = index;
+ return char2;
+ }
+ following--;
+ index++;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Save the data at the current location then move to the next available space.
+ ///
+ /// The byte to be saved.
+ /// true if the operation successes. false if can't find available space.
+ public bool Put(byte data)
+ {
+ if (_block == null)
+ {
+ return false;
+ }
+ else if (_index < _block.End)
+ {
+ _block.Array[_index++] = data;
+ return true;
+ }
+
+ var block = _block;
+ var index = _index;
+ while (true)
+ {
+ if (index < block.End)
+ {
+ _block = block;
+ _index = index + 1;
+ block.Array[index] = data;
+ return true;
+ }
+ else if (block.Next == null)
+ {
+ return false;
+ }
+ else
+ {
+ block = block.Next;
+ index = block.Start;
+ }
+ }
+ }
+
public int GetLength(MemoryPoolIterator2 end)
{
if (IsDefault || end.IsDefault)
diff --git a/test/Microsoft.AspNet.Server.KestrelTests/MemoryPoolIterator2Tests.cs b/test/Microsoft.AspNet.Server.KestrelTests/MemoryPoolIterator2Tests.cs
new file mode 100644
index 0000000000..b7d101c28c
--- /dev/null
+++ b/test/Microsoft.AspNet.Server.KestrelTests/MemoryPoolIterator2Tests.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Linq;
+using Microsoft.AspNet.Server.Kestrel.Infrastructure;
+using Xunit;
+
+namespace Microsoft.AspNet.Server.KestrelTests
+{
+ public class MemoryPoolIterator2Tests : IDisposable
+ {
+ private readonly MemoryPool2 _pool;
+
+ public MemoryPoolIterator2Tests()
+ {
+ _pool = new MemoryPool2();
+ }
+
+ public void Dispose()
+ {
+ _pool.Dispose();
+ }
+
+ [Theory]
+ [InlineData("a", "a", 'a', 0)]
+ [InlineData("ab", "a", 'a', 0)]
+ [InlineData("aab", "a", 'a', 0)]
+ [InlineData("acab", "a", 'a', 0)]
+ [InlineData("acab", "c", 'c', 1)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "lo", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "ol", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "ll", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "rml", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyz", "mlr", 'l', 11)]
+ [InlineData("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
+ [InlineData("aaaaaaaaaaalmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'l', 11)]
+ [InlineData("aaaaaaaaaaacmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'm', 12)]
+ [InlineData("aaaaaaaaaaarmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "lmr", 'r', 11)]
+ [InlineData("/localhost:5000/PATH/%2FPATH2/ HTTP/1.1", " %?", '%', 21)]
+ [InlineData("/localhost:5000/PATH/%2FPATH2/?key=value HTTP/1.1", " %?", '%', 21)]
+ [InlineData("/localhost:5000/PATH/PATH2/?key=value HTTP/1.1", " %?", '?', 27)]
+ [InlineData("/localhost:5000/PATH/PATH2/ HTTP/1.1", " %?", ' ', 27)]
+ public void MemorySeek(string raw, string search, char expectResult, int expectIndex)
+ {
+ var block = _pool.Lease(256);
+ var chars = raw.ToCharArray().Select(c => (byte)c).ToArray();
+ Buffer.BlockCopy(chars, 0, block.Array, block.Start, chars.Length);
+ block.End += chars.Length;
+
+ var begin = block.GetIterator();
+ var searchFor = search.ToCharArray();
+
+ int found = -1;
+ if (searchFor.Length == 1)
+ {
+ found = begin.Seek(searchFor[0]);
+ }
+ else if (searchFor.Length == 2)
+ {
+ found = begin.Seek(searchFor[0], searchFor[1]);
+ }
+ else if (searchFor.Length == 3)
+ {
+ found = begin.Seek(searchFor[0], searchFor[1], searchFor[2]);
+ }
+ else
+ {
+ Assert.False(true, "Invalid test sample.");
+ }
+
+ Assert.Equal(expectResult, found);
+ Assert.Equal(expectIndex, begin.Index - block.Start);
+ }
+
+ [Fact]
+ public void Put()
+ {
+ var blocks = new MemoryPoolBlock2[4];
+ for (var i = 0; i < 4; ++i)
+ {
+ blocks[i] = _pool.Lease(16);
+ 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.False(head.Put(0xFF));
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Server.KestrelTests/UrlPathDecoder.cs b/test/Microsoft.AspNet.Server.KestrelTests/UrlPathDecoder.cs
new file mode 100644
index 0000000000..928fc84a05
--- /dev/null
+++ b/test/Microsoft.AspNet.Server.KestrelTests/UrlPathDecoder.cs
@@ -0,0 +1,172 @@
+// 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.Linq;
+using Microsoft.AspNet.Server.Kestrel.Http;
+using Microsoft.AspNet.Server.Kestrel.Infrastructure;
+using Xunit;
+
+namespace Microsoft.AspNet.Server.KestrelTests
+{
+ public class UrlPathDecoderTests
+ {
+
+ [Fact]
+ public void Empty()
+ {
+ PositiveAssert(string.Empty, string.Empty);
+ }
+
+ [Fact]
+ public void WhiteSpace()
+ {
+ PositiveAssert(" ", " ");
+ }
+
+ [Theory]
+ [InlineData("/foo/bar", "/foo/bar")]
+ [InlineData("/foo/BAR", "/foo/BAR")]
+ [InlineData("/foo/", "/foo/")]
+ [InlineData("/", "/")]
+ public void NormalCases(string raw, string expect)
+ {
+ PositiveAssert(raw, expect);
+ }
+
+ [Theory]
+ [InlineData("%2F", "%2F")]
+ [InlineData("/foo%2Fbar", "/foo%2Fbar")]
+ [InlineData("/foo%2F%20bar", "/foo%2F bar")]
+ public void SkipForwardSlash(string raw, string expect)
+ {
+ PositiveAssert(raw, expect);
+ }
+
+ [Theory]
+ [InlineData("%D0%A4", "Ф")]
+ [InlineData("%d0%a4", "Ф")]
+ [InlineData("%E0%A4%AD", "भ")]
+ [InlineData("%e0%A4%Ad", "भ")]
+ [InlineData("%F0%A4%AD%A2", "𤭢")]
+ [InlineData("%F0%a4%Ad%a2", "𤭢")]
+ [InlineData("%48%65%6C%6C%6F%20%57%6F%72%6C%64", "Hello World")]
+ [InlineData("%48%65%6C%6C%6F%2D%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", "Hello-µ@ßöäüàá")]
+ // Test the borderline cases of overlong UTF8.
+ [InlineData("%C2%80", "\u0080")]
+ [InlineData("%E0%A0%80", "\u0800")]
+ [InlineData("%F0%90%80%80", "\U00010000")]
+ [InlineData("%63", "c")]
+ [InlineData("%32", "2")]
+ [InlineData("%20", " ")]
+ public void ValidUTF8(string raw, string expect)
+ {
+ PositiveAssert(raw, expect);
+ }
+
+ [Theory]
+ [InlineData("%C3%84ra%20Benetton", "Ära Benetton")]
+ [InlineData("%E6%88%91%E8%87%AA%E6%A8%AA%E5%88%80%E5%90%91%E5%A4%A9%E7%AC%91%E5%8E%BB%E7%95%99%E8%82%9D%E8%83%86%E4%B8%A4%E6%98%86%E4%BB%91", "我自横刀向天笑去留肝胆两昆仑")]
+ public void Internationalized(string raw, string expect)
+ {
+ PositiveAssert(raw, expect);
+ }
+
+ [Theory]
+ // Overlong ASCII
+ [InlineData("%C0%A4", "%C0%A4")]
+ [InlineData("%C1%BF", "%C1%BF")]
+ [InlineData("%E0%80%AF", "%E0%80%AF")]
+ [InlineData("%E0%9F%BF", "%E0%9F%BF")]
+ [InlineData("%F0%80%80%AF", "%F0%80%80%AF")]
+ [InlineData("%F0%8F%8F%BF", "%F0%8F%8F%BF")]
+ // Incomplete
+ [InlineData("%", "%")]
+ [InlineData("%%", "%%")]
+ [InlineData("%A", "%A")]
+ [InlineData("%Y", "%Y")]
+ // Mixed
+ [InlineData("%%32", "%2")]
+ [InlineData("%%20", "% ")]
+ [InlineData("%C0%A4%32", "%C0%A42")]
+ [InlineData("%32%C0%A4%32", "2%C0%A42")]
+ [InlineData("%C0%32%A4", "%C02%A4")]
+ public void InvalidUTF8(string raw, string expect)
+ {
+ PositiveAssert(raw, expect);
+ }
+
+ [Theory]
+ [InlineData("/foo%2Fbar", 10, "/foo%2Fbar", 10)]
+ [InlineData("/foo%2Fbar", 9, "/foo%2Fba", 9)]
+ [InlineData("/foo%2Fbar", 8, "/foo%2Fb", 8)]
+ [InlineData("%D0%A4", 6, "Ф", 1)]
+ [InlineData("%D0%A4", 5, "%D0%A", 5)]
+ [InlineData("%D0%A4", 4, "%D0%", 4)]
+ [InlineData("%D0%A4", 3, "%D0", 3)]
+ [InlineData("%D0%A4", 2, "%D", 2)]
+ [InlineData("%D0%A4", 1, "%", 1)]
+ [InlineData("%D0%A4", 0, "", 0)]
+ [InlineData("%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", 45, "µ@ßöäüàá", 8)]
+ [InlineData("%C2%B5%40%C3%9F%C3%B6%C3%A4%C3%BC%C3%A0%C3%A1", 44, "µ@ßöäüà%C3%A", 12)]
+ public void DecodeWithBoundary(string raw, int rawLength, string expect, int expectLength)
+ {
+ var begin = BuildSample(raw);
+ var end = GetIterator(begin, rawLength);
+
+ var end2 = UrlPathDecoder.Unescape(begin, end);
+ var result = begin.GetString(end2);
+
+ Assert.Equal(expectLength, result.Length);
+ Assert.Equal(expect, result);
+ }
+
+ private MemoryPoolIterator2 BuildSample(string data)
+ {
+ var store = data.Select(c => (byte)c).ToArray();
+ var mem = MemoryPoolBlock2.Create(new ArraySegment(store), IntPtr.Zero, null, null);
+ mem.End = store.Length;
+
+ return mem.GetIterator();
+ }
+
+ private MemoryPoolIterator2 GetIterator(MemoryPoolIterator2 begin, int displacement)
+ {
+ var result = begin;
+ for (int i = 0; i < displacement; ++i)
+ {
+ result.Take();
+ }
+
+ return result;
+ }
+
+ private void PositiveAssert(string raw, string expect)
+ {
+ var begin = BuildSample(raw);
+ var end = GetIterator(begin, raw.Length);
+
+ var result = UrlPathDecoder.Unescape(begin, end);
+ Assert.Equal(expect, begin.GetString(result));
+ }
+
+ private void PositiveAssert(string raw)
+ {
+ var begin = BuildSample(raw);
+ var end = GetIterator(begin, raw.Length);
+
+ var result = UrlPathDecoder.Unescape(begin, end);
+ Assert.NotEqual(raw.Length, begin.GetString(result).Length);
+ }
+
+ private void NegativeAssert(string raw)
+ {
+ var begin = BuildSample(raw);
+ var end = GetIterator(begin, raw.Length);
+
+ var resultEnd = UrlPathDecoder.Unescape(begin, end);
+ var result = begin.GetString(resultEnd);
+ Assert.Equal(raw, result);
+ }
+ }
+}