| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Diagnostics; |
| | | 6 | | using System.Runtime.InteropServices; |
| | | 7 | | using System.Text; |
| | | 8 | | |
| | | 9 | | namespace System |
| | | 10 | | { |
| | | 11 | | internal static class PercentEncodingHelper |
| | | 12 | | { |
| | | 13 | | public static int UnescapePercentEncodedUTF8Sequence(scoped ReadOnlySpan<char> input, ref ValueStringBuilder des |
| | 0 | 14 | | { |
| | | 15 | | // As an optimization, this method should only be called after the first character is known to be a part of |
| | 0 | 16 | | Debug.Assert(input.Length >= 3); |
| | 0 | 17 | | Debug.Assert(input[0] == '%'); |
| | 0 | 18 | | Debug.Assert(UriHelper.DecodeHexChars(input[1], input[2]) != Uri.c_DummyChar); |
| | 0 | 19 | | Debug.Assert(UriHelper.DecodeHexChars(input[1], input[2]) >= 128); |
| | | 20 | | |
| | 0 | 21 | | uint fourByteBuffer = 0; |
| | 0 | 22 | | int bytesLeftInBuffer = 0; |
| | | 23 | | |
| | 0 | 24 | | int totalCharsConsumed = 0; |
| | 0 | 25 | | int charsToCopy = 0; |
| | 0 | 26 | | int bytesConsumed = 0; |
| | | 27 | | |
| | 0 | 28 | | RefillBuffer: |
| | 0 | 29 | | int i = totalCharsConsumed + (bytesLeftInBuffer * 3); |
| | | 30 | | |
| | 0 | 31 | | ReadByteFromInput: |
| | 0 | 32 | | if ((uint)(i + 2) >= (uint)input.Length || input[i] != '%') |
| | 0 | 33 | | goto NoMoreOrInvalidInput; |
| | | 34 | | |
| | 0 | 35 | | uint value = (uint)HexConverter.FromChar(input[i + 1]); |
| | | 36 | | |
| | | 37 | | // Check if the first character is 0 to avoid the case where "%0_" passes the "(value - 128) > 127" guard be |
| | | 38 | | // an invalid second character decodes to 0xFF on its own. If we exclude 0, all invalid values will map outs |
| | 0 | 39 | | if (value == 0) |
| | 0 | 40 | | goto NoMoreOrInvalidInput; |
| | | 41 | | |
| | 0 | 42 | | value = (value << 4) + (uint)HexConverter.FromChar(input[i + 2]); |
| | | 43 | | |
| | 0 | 44 | | if ((value - 128) > 127) |
| | 0 | 45 | | goto NoMoreOrInvalidInput; // Either not Hex or the decoded value is ASCII |
| | | 46 | | |
| | | 47 | | // Rotate the buffer and overwrite the last byte |
| | 0 | 48 | | if (BitConverter.IsLittleEndian) |
| | 0 | 49 | | { |
| | 0 | 50 | | fourByteBuffer = (fourByteBuffer >> 8) | (value << 24); |
| | 0 | 51 | | } |
| | | 52 | | else |
| | 0 | 53 | | { |
| | 0 | 54 | | fourByteBuffer = (fourByteBuffer << 8) | value; |
| | 0 | 55 | | } |
| | | 56 | | |
| | 0 | 57 | | if (++bytesLeftInBuffer != 4) |
| | 0 | 58 | | { |
| | 0 | 59 | | i += 3; |
| | 0 | 60 | | goto ReadByteFromInput; |
| | | 61 | | } |
| | | 62 | | |
| | 0 | 63 | | DecodeRune: |
| | 0 | 64 | | Debug.Assert(totalCharsConsumed % 3 == 0); |
| | 0 | 65 | | Debug.Assert(bytesLeftInBuffer == 2 || bytesLeftInBuffer == 3 || bytesLeftInBuffer == 4); |
| | 0 | 66 | | Debug.Assert((fourByteBuffer & (BitConverter.IsLittleEndian ? 0x00000080 : 0x80000000)) != 0); |
| | 0 | 67 | | Debug.Assert((fourByteBuffer & (BitConverter.IsLittleEndian ? 0x00008000 : 0x00800000)) != 0); |
| | 0 | 68 | | Debug.Assert(bytesLeftInBuffer < 3 || (fourByteBuffer & (BitConverter.IsLittleEndian ? 0x00800000 : 0x000080 |
| | 0 | 69 | | Debug.Assert(bytesLeftInBuffer < 4 || (fourByteBuffer & (BitConverter.IsLittleEndian ? 0x80000000 : 0x000000 |
| | | 70 | | |
| | 0 | 71 | | uint temp = fourByteBuffer; // make a copy so that the *copy* (not the original) is marked address-taken |
| | | 72 | | |
| | 0 | 73 | | if (Rune.DecodeFromUtf8(MemoryMarshal.AsBytes(new ReadOnlySpan<uint>(ref temp))[..bytesLeftInBuffer], out Ru |
| | 0 | 74 | | { |
| | 0 | 75 | | Debug.Assert(bytesConsumed >= 2, $"Rune.DecodeFromUtf8 consumed {bytesConsumed} bytes, likely indicating |
| | | 76 | | |
| | 0 | 77 | | if (!iriParsing || IriHelper.CheckIriUnicodeRange((uint)rune.Value, isQuery)) |
| | 0 | 78 | | { |
| | 0 | 79 | | if (charsToCopy != 0) |
| | 0 | 80 | | { |
| | 0 | 81 | | dest.Append(input.Slice(totalCharsConsumed - charsToCopy, charsToCopy)); |
| | 0 | 82 | | charsToCopy = 0; |
| | 0 | 83 | | } |
| | | 84 | | |
| | 0 | 85 | | dest.Append(rune); |
| | 0 | 86 | | goto AfterDecodeRune; |
| | | 87 | | } |
| | 0 | 88 | | } |
| | | 89 | | else |
| | 0 | 90 | | { |
| | 0 | 91 | | Debug.Assert(bytesConsumed > 0, $"Rune.DecodeFromUtf8 consumed {bytesConsumed} bytes when decoding {byte |
| | 0 | 92 | | } |
| | 0 | 93 | | charsToCopy += bytesConsumed * 3; |
| | | 94 | | |
| | 0 | 95 | | AfterDecodeRune: |
| | 0 | 96 | | bytesLeftInBuffer -= bytesConsumed; |
| | 0 | 97 | | totalCharsConsumed += bytesConsumed * 3; |
| | 0 | 98 | | goto RefillBuffer; |
| | | 99 | | |
| | 0 | 100 | | NoMoreOrInvalidInput: |
| | 0 | 101 | | Debug.Assert(bytesLeftInBuffer < 4); |
| | | 102 | | |
| | | 103 | | // If we have more than 1 byte left, we try to decode it |
| | 0 | 104 | | if (bytesLeftInBuffer > 1) |
| | 0 | 105 | | { |
| | 0 | 106 | | Debug.Assert(bytesLeftInBuffer == 2 || bytesLeftInBuffer == 3); |
| | | 107 | | |
| | | 108 | | // We reach this branch if we don't have 4 valid bytes to consume |
| | | 109 | | // We have to allign the read bytes to the start of fourByteBuffer memory |
| | | 110 | | // We do this by shifting the fourByteBuffer, the shift direction is determined by system endianness |
| | | 111 | | |
| | | 112 | | // If we read 3 bytes, we shift by 1; if we read 2, we shift by 2 |
| | | 113 | | // (32 - (bytesLeftInBuffer << 3)) calculates this offset: |
| | | 114 | | // bytesLeftInBuffer == 3 => (32 - (3 << 3)) => 32 - 24 => 8 bits |
| | | 115 | | // bytesLeftInBuffer == 2 => (32 - (2 << 3)) => 32 - 16 => 16 bits |
| | | 116 | | |
| | | 117 | | // For invalid input we tried to decode in DecodeRune, we may return here if we have more than 1 byte le |
| | | 118 | | // If bytesConsumed is 1, shift by 1 byte |
| | | 119 | | // If bytesConsumed is 2: |
| | | 120 | | // a) We had 4 bytes in the buffer and now only have 2 => Shift by 2 bytes |
| | | 121 | | // b) We read 1 more byte, leaving us with 3 bytes in the buffer => Shift by 1 byte |
| | | 122 | | // The case for bytesConsumed == 2 is handled by the else block as the offsets are the same as for valid |
| | | 123 | | |
| | 0 | 124 | | if (bytesConsumed == 1) |
| | 0 | 125 | | { |
| | 0 | 126 | | if (BitConverter.IsLittleEndian) |
| | 0 | 127 | | { |
| | 0 | 128 | | fourByteBuffer >>= 8; |
| | 0 | 129 | | } |
| | | 130 | | else |
| | 0 | 131 | | { |
| | 0 | 132 | | fourByteBuffer <<= 8; |
| | 0 | 133 | | } |
| | 0 | 134 | | } |
| | | 135 | | else |
| | 0 | 136 | | { |
| | 0 | 137 | | if (BitConverter.IsLittleEndian) |
| | 0 | 138 | | { |
| | 0 | 139 | | fourByteBuffer >>= (32 - (bytesLeftInBuffer << 3)); |
| | 0 | 140 | | } |
| | | 141 | | else |
| | 0 | 142 | | { |
| | 0 | 143 | | fourByteBuffer <<= (32 - (bytesLeftInBuffer << 3)); |
| | 0 | 144 | | } |
| | 0 | 145 | | } |
| | 0 | 146 | | goto DecodeRune; |
| | | 147 | | } |
| | | 148 | | |
| | 0 | 149 | | Debug.Assert(bytesLeftInBuffer == 0 || bytesLeftInBuffer == 1); |
| | | 150 | | |
| | 0 | 151 | | if ((bytesLeftInBuffer | charsToCopy) == 0) |
| | 0 | 152 | | return totalCharsConsumed; |
| | | 153 | | |
| | 0 | 154 | | bytesLeftInBuffer *= 3; |
| | | 155 | | |
| | 0 | 156 | | dest.Append(input.Slice(totalCharsConsumed - charsToCopy, charsToCopy + bytesLeftInBuffer)); |
| | 0 | 157 | | return totalCharsConsumed + bytesLeftInBuffer; |
| | 0 | 158 | | } |
| | | 159 | | } |
| | | 160 | | } |
| | | 161 | | |