From 52f4fa91e3f68c5f8a2e726432d85806024fd834 Mon Sep 17 00:00:00 2001 From: Troy Dai Date: Wed, 21 Oct 2015 14:52:14 -0700 Subject: [PATCH] Unescape string in memory 1. In place unescape; 1. UTF-8 verification; 2. MemoryPoolIterator2.Put 3. Tests --- .../Http/Frame.cs | 26 +- .../Http/UrlPathDecoder.cs | 306 ++++++++++++++++++ .../Infrastructure/MemoryPoolIterator2.cs | 170 +++++++++- .../MemoryPoolIterator2Tests.cs | 132 ++++++++ .../UrlPathDecoder.cs | 172 ++++++++++ 5 files changed, 796 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Http/UrlPathDecoder.cs create mode 100644 test/Microsoft.AspNet.Server.KestrelTests/MemoryPoolIterator2Tests.cs create mode 100644 test/Microsoft.AspNet.Server.KestrelTests/UrlPathDecoder.cs diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs index 0d7b5ce6e..79552ea94 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs @@ -420,8 +420,8 @@ public async Task ProduceStartAndFireOnStarting(bool immediate = true) 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 @@ private bool TakeStartLine(SocketInput input) 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 @@ private bool TakeStartLine(SocketInput input) 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 000000000..66608a4a2 --- /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 8964093d0..7de5c7fb1 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 @@ public struct MemoryPoolIterator2 { /// /// 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 @@ public int Seek(int char0, int char1) } } + 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 000000000..b7d101c28 --- /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 000000000..928fc84a0 --- /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); + } + } +}