< Summary

Information
Class: NanoRoute.Internals.UrlUtils
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/LowLevel/UrlUtils.cs
Line coverage
100%
Covered lines: 92
Uncovered lines: 0
Coverable lines: 92
Total lines: 180
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Private/LowLevel/UrlUtils.cs

#LineLine coverage
 1/********************************************************************************
 2* UrlUtils.cs                                                                   *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Text;
 8
 9namespace NanoRoute.Internals
 10{
 11    using Properties;
 12
 13    internal enum UrlDecodeMode
 14    {
 15        Path,
 16        Form
 17    }
 18
 19    internal static class UrlUtils
 20    {
 121        private static readonly Encoding s_utf8 = new UTF8Encoding(false, true);
 22
 23        public static ReadOnlyMemory<char> DecodeUrl(ReadOnlyMemory<char> source, UrlDecodeMode mode)
 224        {
 225            char[] result = new char[source.Length];
 26
 227            if (!TryDecodeUrl(source.Span, result.AsSpan(), mode, out int charsWritten))
 128                throw new InvalidOperationException(Resources.ERR_DECODING_FAILED);
 29
 230            return result.AsMemory(0, charsWritten);
 231        }
 32
 33        public static bool TryDecodeUrl(ReadOnlySpan<char> source, Span<char> destination, UrlDecodeMode mode, out int c
 234        {
 235            charsWritten = 0;
 36
 237            for (int i = 0; i < source.Length;)
 238            {
 239                if (charsWritten >= destination.Length)
 140                    return false;
 41
 242                switch (source[i])
 43                {
 144                    case '+' when mode is UrlDecodeMode.Form:
 145                        destination[charsWritten++] = ' ';
 146                        i++;
 147                        break;
 48
 49                    case '%':
 150                        if (!TryDecodeUtf8Sequence(source, ref i, destination, ref charsWritten))
 151                            return false;
 152                        break;
 53
 54                    default:
 255                        if (!TryCopyLiteralSegment(source, mode, ref i, destination, ref charsWritten))
 156                            return false;
 257                        break;
 58                }
 259            }
 60
 261            return true;
 262        }
 63
 64        private static bool TryCopyLiteralSegment(ReadOnlySpan<char> source, UrlDecodeMode mode, ref int offset, Span<ch
 265        {
 266            ReadOnlySpan<char> segment = source.Slice(offset);
 67
 268            int special = mode is UrlDecodeMode.Form ? segment.IndexOfAny('%', '+') : segment.IndexOf('%');
 269            if (special > 0)
 170                segment = segment.Slice(0, special);
 71
 272            if (charsWritten + segment.Length > destination.Length)
 173                return false;
 74
 275            segment.CopyTo(destination.Slice(charsWritten));
 276            charsWritten += segment.Length;
 277            offset += segment.Length;
 78
 279            return true;
 280        }
 81
 82        private static bool TryDecodeUtf8Sequence(ReadOnlySpan<char> source, ref int offset, Span<char> destination, ref
 183        {
 84            const int ESCAPED_BYTE_LENGTH = 3; // "%XX"
 85#if NETSTANDARD2_1_OR_GREATER
 186            Span<byte> bytes = stackalloc byte[4];
 87#else
 88            byte[] bytes = new byte[4];
 89#endif
 90            // The first escaped byte determines how many %XX chunks belong to this UTF-8 sequence.
 191            if (!TryParseEscapedByte(source, offset, out bytes[0]))
 192                return false;
 93
 194            int byteCount = GetUtf8ByteCount(bytes[0]);
 195            if (byteCount < 0)
 196                return false;
 97
 98            // Collect the remaining escaped bytes, then let the strict UTF-8 decoder validate them.
 199            for (int i = 1; i < byteCount; i++)
 1100                if (!TryParseEscapedByte(source, offset + i * ESCAPED_BYTE_LENGTH, out bytes[i]))
 1101                    return false;
 102
 103#if NETSTANDARD2_1_OR_GREATER
 1104            Span<char> chars = stackalloc char[2];
 105#else
 106            char[] chars = new char[2];
 107#endif
 108            int decodedChars;
 109
 110            try
 1111            {
 112#if NETSTANDARD2_1_OR_GREATER
 1113                decodedChars = s_utf8.GetChars(bytes.Slice(0, byteCount), chars);
 114#else
 115                decodedChars = s_utf8.GetChars(bytes, 0, byteCount, chars, 0);
 116#endif
 1117            }
 1118            catch (DecoderFallbackException)
 1119            {
 1120                return false;
 121            }
 122
 1123            if (charsWritten + decodedChars > destination.Length)
 1124                return false;
 125
 1126            chars
 1127#if NETSTANDARD2_0
 1128                .AsSpan(0, decodedChars)
 1129#endif
 1130                .CopyTo(destination.Slice(charsWritten));
 1131            charsWritten += decodedChars;
 1132            offset += byteCount * ESCAPED_BYTE_LENGTH;
 133
 1134            return true;
 1135        }
 136
 137        private static bool TryParseEscapedByte(ReadOnlySpan<char> source, int offset, out byte value)
 1138        {
 1139            value = default;
 140
 1141            return offset >= 0 &&
 1142                   offset + 2 < source.Length &&
 1143                   source[offset] is '%' &&
 1144                   TryParseHex2(source[offset + 1], source[offset + 2], out value);
 1145        }
 146
 1147        private static int GetUtf8ByteCount(byte first) => first switch
 1148        {
 1149            <= 0x7F => 1,
 1150            >= 0xC2 and <= 0xDF => 2,
 1151            >= 0xE0 and <= 0xEF => 3,
 1152            >= 0xF0 and <= 0xF4 => 4,
 1153            _ => -1
 1154        };
 155
 156        private static bool TryParseHex2(char high, char low, out byte value)
 1157        {
 1158            value = default;
 159
 1160            int
 1161                hi = HexToInt(high),
 1162                lo = HexToInt(low);
 163
 1164            if (hi < 0 || lo < 0)
 1165                return false;
 166
 1167            value = (byte) ((hi << 4) | lo);
 1168            return true;
 169
 170            static int HexToInt(char c)
 171            {
 172                if ((uint)(c - '0') <= 9) return c - '0';
 173                if ((uint)(c - 'a') <= 5) return c - 'a' + 10;
 174                if ((uint)(c - 'A') <= 5) return c - 'A' + 10;
 175                return -1;
 176            }
 1177        }
 178    }
 179}
 180