| | | 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.Diagnostics; |
| | | 5 | | using System.Diagnostics.CodeAnalysis; |
| | | 6 | | using System.Numerics; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Runtime.Intrinsics; |
| | | 9 | | using System.Runtime.Intrinsics.Arm; |
| | | 10 | | |
| | | 11 | | namespace System.Text.Unicode |
| | | 12 | | { |
| | | 13 | | internal static unsafe partial class Utf16Utility |
| | | 14 | | { |
| | | 15 | | |
| | | 16 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 17 | | private static nuint GetSurrogateMask(Vector128<ushort> cmp) |
| | | 18 | | { |
| | | 19 | | // Convert the comparison result to a scalar surrogate mask. |
| | | 20 | | // The elements in 'cmp' should be either all bits set or zero. |
| | | 21 | | |
| | | 22 | | if (AdvSimd.Arm64.IsSupported) |
| | | 23 | | { |
| | | 24 | | // Since ExtractMostSignificantBits is very slow on AdvSimd, |
| | | 25 | | // we use a 64-bit value to encode the mask, where each byte represents one element: |
| | | 26 | | // 0x01 for all bits set, 0x00 for zero. |
| | | 27 | | ulong mask = AdvSimd.Arm64.UnzipOdd(cmp.AsByte(), cmp.AsByte()).AsUInt64().ToScalar(); |
| | | 28 | | return (nuint)(mask & 0x0101010101010101u); |
| | | 29 | | } |
| | | 30 | | |
| | | 31 | | // Otherwise, encode the mask with 8-bits (one byte), where each bit represents one element. |
| | 0 | 32 | | return cmp.ExtractMostSignificantBits(); |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 36 | | private static bool IsSurrogatesMatch(nuint maskHigh, nuint maskLow) |
| | | 37 | | { |
| | | 38 | | // Make sure that each high surrogate is followed by a low surrogate character, |
| | | 39 | | // and each low surrogate follows a high surrogate character. |
| | | 40 | | // The last character is discarded as it will be checked by 'IsLastCharHighSurrogate'. |
| | | 41 | | // The first character must not be a low surrogate. This is checked by matching |
| | | 42 | | // 'maskLow' aganist the zeros inserted after shifting 'maskHigh' to the left. |
| | | 43 | | |
| | | 44 | | if (AdvSimd.Arm64.IsSupported) |
| | | 45 | | { |
| | | 46 | | // Each surrogate character is 8 bits apart. |
| | | 47 | | return (maskHigh << 8) == maskLow; |
| | | 48 | | } |
| | | 49 | | // Each surrogate character is 1 bit apart. |
| | 0 | 50 | | return (byte)(maskHigh << 1) == (byte)maskLow; |
| | | 51 | | } |
| | | 52 | | |
| | | 53 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 54 | | private static bool IsLastCharHighSurrogate(nuint maskHigh) |
| | | 55 | | { |
| | | 56 | | if (AdvSimd.Arm64.IsSupported) |
| | | 57 | | { |
| | | 58 | | // Check if the top byte is not zero. |
| | | 59 | | return (maskHigh >>> 56) != 0; |
| | | 60 | | } |
| | | 61 | | // Check if the top bit (of a byte) is not zero. |
| | 0 | 62 | | return ((byte)maskHigh >>> 7) != 0; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | // Returns &inputBuffer[inputLength] if the input buffer is valid. |
| | | 66 | | /// <summary> |
| | | 67 | | /// Given an input buffer <paramref name="pInputBuffer"/> of char length <paramref name="inputLength"/>, |
| | | 68 | | /// returns a pointer to where the first invalid data appears in <paramref name="pInputBuffer"/>. |
| | | 69 | | /// </summary> |
| | | 70 | | /// <remarks> |
| | | 71 | | /// Returns a pointer to the end of <paramref name="pInputBuffer"/> if the buffer is well-formed. |
| | | 72 | | /// </remarks> |
| | | 73 | | public static char* GetPointerToFirstInvalidChar(char* pInputBuffer, int inputLength, out long utf8CodeUnitCount |
| | | 74 | | { |
| | | 75 | | Debug.Assert(inputLength >= 0, "Input length must not be negative."); |
| | 16 | 76 | | Debug.Assert(pInputBuffer != null || inputLength == 0, "Input length must be zero if input buffer pointer is |
| | | 77 | | |
| | | 78 | | // First, we'll handle the common case of all-ASCII. If this is able to |
| | | 79 | | // consume the entire buffer, we'll skip the remainder of this method's logic. |
| | | 80 | | |
| | 16 | 81 | | int numAsciiCharsConsumedJustNow = (int)Ascii.GetIndexOfFirstNonAsciiChar(pInputBuffer, (uint)inputLength); |
| | 16 | 82 | | Debug.Assert(0 <= numAsciiCharsConsumedJustNow && numAsciiCharsConsumedJustNow <= inputLength); |
| | | 83 | | |
| | 16 | 84 | | pInputBuffer += (uint)numAsciiCharsConsumedJustNow; |
| | 16 | 85 | | inputLength -= numAsciiCharsConsumedJustNow; |
| | | 86 | | |
| | 16 | 87 | | if (inputLength == 0) |
| | | 88 | | { |
| | 16 | 89 | | utf8CodeUnitCountAdjustment = 0; |
| | 16 | 90 | | scalarCountAdjustment = 0; |
| | 16 | 91 | | return pInputBuffer; |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | // If we got here, it means we saw some non-ASCII data, so within our |
| | | 95 | | // vectorized code paths below we'll handle all non-surrogate UTF-16 |
| | | 96 | | // code points branchlessly. We'll only branch if we see surrogates. |
| | | 97 | | // |
| | | 98 | | // We still optimistically assume the data is mostly ASCII. This means that the |
| | | 99 | | // number of UTF-8 code units and the number of scalars almost matches the number |
| | | 100 | | // of UTF-16 code units. As we go through the input and find non-ASCII |
| | | 101 | | // characters, we'll keep track of these "adjustment" fixups. To get the |
| | | 102 | | // total number of UTF-8 code units required to encode the input data, add |
| | | 103 | | // the UTF-8 code unit count adjustment to the number of UTF-16 code units |
| | | 104 | | // seen. To get the total number of scalars present in the input data, |
| | | 105 | | // add the scalar count adjustment to the number of UTF-16 code units seen. |
| | | 106 | | |
| | 0 | 107 | | long tempUtf8CodeUnitCountAdjustment = 0; |
| | 0 | 108 | | int tempScalarCountAdjustment = 0; |
| | 0 | 109 | | char* pEndOfInputBuffer = pInputBuffer + (uint)inputLength; |
| | | 110 | | |
| | 0 | 111 | | if (Vector128.IsHardwareAccelerated) |
| | | 112 | | { |
| | 0 | 113 | | if (inputLength >= Vector128<ushort>.Count) |
| | | 114 | | { |
| | 0 | 115 | | Vector128<ushort> vector0080 = Vector128.Create<ushort>(0x0080); |
| | 0 | 116 | | Vector128<ushort> vector0400 = Vector128.Create<ushort>(0x0400); |
| | 0 | 117 | | Vector128<ushort> vector0800 = Vector128.Create<ushort>(0x0800); |
| | 0 | 118 | | Vector128<ushort> vectorD800 = Vector128.Create<ushort>(0xD800); |
| | | 119 | | |
| | 0 | 120 | | char* pHighestAddressWhereCanReadOneVector = pEndOfInputBuffer - Vector128<ushort>.Count; |
| | 0 | 121 | | Debug.Assert(pHighestAddressWhereCanReadOneVector >= pInputBuffer); |
| | | 122 | | |
| | | 123 | | do |
| | | 124 | | { |
| | 0 | 125 | | Vector128<ushort> utf16Data = Vector128.Load((ushort*)pInputBuffer); |
| | | 126 | | |
| | | 127 | | // Calculate the popcnt for UTF-8 adjustments, which is the number of *additional* |
| | | 128 | | // UTF-8 bytes that each UTF-16 code unit requires as it expands. |
| | | 129 | | // This results in the wrong count for UTF-16 surrogate code units (we just counted |
| | | 130 | | // that each individual code unit expands to 3 bytes, but in reality a well-formed |
| | | 131 | | // UTF-16 surrogate pair expands to 4 bytes). We'll handle this in just a moment. |
| | | 132 | | // |
| | | 133 | | // For now, compute the popcnt but squirrel it away. We'll fold it in to the |
| | | 134 | | // cumulative UTF-8 adjustment factor once we determine that there are no |
| | | 135 | | // unpaired surrogates in our data. (Unpaired surrogates would invalidate |
| | | 136 | | // our computed result and we'd have to throw it away.) |
| | | 137 | | |
| | | 138 | | uint popcnt; |
| | | 139 | | |
| | | 140 | | // On AdvSimd ExtractMostSignificantBits is very slow, so a different algorithm is used to avoid |
| | | 141 | | // the poor performance. |
| | | 142 | | |
| | | 143 | | if (AdvSimd.Arm64.IsSupported) |
| | | 144 | | { |
| | | 145 | | // The 'twoOrMoreUtf8Bytes' and 'threeOrMoreUtf8Bytes' vectors will contain |
| | | 146 | | // elements whose values are 0xFFFF (-1 as signed word) iff the corresponding |
| | | 147 | | // UTF-16 code unit was >= 0x0080 and >= 0x0800, respectively. By summing these |
| | | 148 | | // vectors, each element of the sum will contain one of three values: |
| | | 149 | | // |
| | | 150 | | // 0x0000 ( 0) = original char was 0000..007F |
| | | 151 | | // 0xFFFF (-1) = original char was 0080..07FF |
| | | 152 | | // 0xFFFE (-2) = original char was 0800..FFFF |
| | | 153 | | // |
| | | 154 | | // We'll negate them to produce a value 0..2 for each element, then sum all the |
| | | 155 | | // elements together to produce the number of *additional* UTF-8 code units |
| | | 156 | | // required to represent this UTF-16 data. |
| | | 157 | | |
| | | 158 | | Vector128<ushort> twoOrMoreUtf8Bytes = Vector128.GreaterThanOrEqual(utf16Data, vector0080); |
| | | 159 | | Vector128<ushort> threeOrMoreUtf8Bytes = Vector128.GreaterThanOrEqual(utf16Data, vector0800) |
| | | 160 | | Vector128<ushort> sumVector = Vector128<ushort>.Zero - twoOrMoreUtf8Bytes - threeOrMoreUtf8B |
| | | 161 | | popcnt = Vector128.Sum(sumVector); |
| | | 162 | | } |
| | | 163 | | else |
| | | 164 | | { |
| | 0 | 165 | | Vector128<ushort> vector7800 = Vector128.Create<ushort>(0x7800); |
| | | 166 | | |
| | | 167 | | // Sets the 0x0080 bit of each element in 'charIsNonAscii' if the corresponding |
| | | 168 | | // input was 0x0080 <= [value]. (i.e., [value] is non-ASCII.) |
| | | 169 | | |
| | 0 | 170 | | Vector128<ushort> charIsNonAscii = Vector128.Min(utf16Data, vector0080); |
| | | 171 | | |
| | | 172 | | #if DEBUG |
| | | 173 | | // Quick check to ensure we didn't accidentally set the 0x8000 bit of any element. |
| | 0 | 174 | | uint debugMask = charIsNonAscii.AsByte().ExtractMostSignificantBits(); |
| | 0 | 175 | | Debug.Assert((debugMask & 0b_1010_1010_1010_1010) == 0, "Shouldn't have set the 0x8000 bit o |
| | | 176 | | #endif // DEBUG |
| | | 177 | | |
| | | 178 | | // Sets the 0x8080 bits of each element in 'charIsNonAscii' if the corresponding |
| | | 179 | | // input was 0x0800 <= [value]. This also handles the missing range a few lines above. |
| | | 180 | | // Since 3-byte elements have a value >= 0x0800, we'll perform a saturating add of 0x7800 in |
| | | 181 | | // get all 3-byte elements to have their 0x8000 bits set. A saturating add will not set the |
| | | 182 | | // bit for 1-byte or 2-byte elements. The 0x0080 bit will already have been set for non-ASCI |
| | | 183 | | // and 3-byte) elements. |
| | | 184 | | |
| | 0 | 185 | | Vector128<ushort> charIsThreeByteUtf8Encoded = Vector128.AddSaturate(utf16Data, vector7800); |
| | | 186 | | |
| | | 187 | | // Each even bit of mask will be 1 only if the char was >= 0x0080, |
| | | 188 | | // and each odd bit of mask will be 1 only if the char was >= 0x0800. |
| | | 189 | | // |
| | | 190 | | // Example for UTF-16 input "[ 0123 ] [ 1234 ] ...": |
| | | 191 | | // |
| | | 192 | | // ,-- set if char[1] is >= 0x0800 |
| | | 193 | | // | ,-- set if char[0] is >= 0x0800 |
| | | 194 | | // v v |
| | | 195 | | // mask = ... 1 1 0 1 |
| | | 196 | | // ^ ^-- set if char[0] is non-ASCII |
| | | 197 | | // `-- set if char[1] is non-ASCII |
| | | 198 | | |
| | 0 | 199 | | uint mask = (charIsNonAscii | charIsThreeByteUtf8Encoded).AsByte().ExtractMostSignificantBit |
| | 0 | 200 | | popcnt = (uint)BitOperations.PopCount(mask); // on x64, perform zero-extension for free |
| | | 201 | | } |
| | | 202 | | |
| | | 203 | | // Now check for surrogates. |
| | | 204 | | |
| | 0 | 205 | | utf16Data -= vectorD800; |
| | 0 | 206 | | nuint maskSurr = GetSurrogateMask(Vector128.LessThan(utf16Data, vector0800)); |
| | 0 | 207 | | if (maskSurr != 0) |
| | | 208 | | { |
| | | 209 | | // Get the surrogate masks for high and low surrogates. |
| | | 210 | | // A high surrogate will be less than 0x0400 after subtracting by 0xD800. |
| | | 211 | | // A low surrogate is a surrogate that is not a high surrogate. |
| | | 212 | | |
| | 0 | 213 | | nuint maskHigh = GetSurrogateMask(Vector128.LessThan(utf16Data, vector0400)); |
| | 0 | 214 | | nuint maskLow = ~maskHigh & maskSurr; |
| | | 215 | | |
| | 0 | 216 | | if (!IsSurrogatesMatch(maskHigh, maskLow)) |
| | | 217 | | { |
| | | 218 | | break; // error: mismatched surrogate pair; break out of vectorized logic |
| | | 219 | | } |
| | | 220 | | |
| | 0 | 221 | | if (IsLastCharHighSurrogate(maskHigh)) |
| | | 222 | | { |
| | | 223 | | // There was a standalone high surrogate at the end of the vector. |
| | | 224 | | // We'll adjust our counters so that we don't consider this char consumed. |
| | | 225 | | |
| | 0 | 226 | | pInputBuffer--; |
| | 0 | 227 | | popcnt -= 2; |
| | | 228 | | } |
| | | 229 | | |
| | | 230 | | // If all the surrogate pairs are valid, then the number of surrogate pairs |
| | | 231 | | // is equal to the number of low surrogates. |
| | | 232 | | |
| | 0 | 233 | | nint surrogatePairsCountNint = (nint)BitOperations.PopCount(maskLow); |
| | | 234 | | |
| | | 235 | | // 2 UTF-16 chars become 1 Unicode scalar |
| | | 236 | | |
| | 0 | 237 | | tempScalarCountAdjustment -= (int)surrogatePairsCountNint; |
| | | 238 | | |
| | | 239 | | // Since each surrogate code unit was >= 0x0800, we eagerly assumed |
| | | 240 | | // it'd be encoded as 3 UTF-8 code units. Each surrogate half is only |
| | | 241 | | // encoded as 2 UTF-8 code units (for 4 UTF-8 code units total), |
| | | 242 | | // so we'll adjust this now. |
| | | 243 | | |
| | 0 | 244 | | tempUtf8CodeUnitCountAdjustment -= surrogatePairsCountNint; |
| | 0 | 245 | | tempUtf8CodeUnitCountAdjustment -= surrogatePairsCountNint; |
| | | 246 | | } |
| | | 247 | | |
| | 0 | 248 | | tempUtf8CodeUnitCountAdjustment += popcnt; |
| | 0 | 249 | | pInputBuffer += Vector128<ushort>.Count; |
| | 0 | 250 | | } while (pInputBuffer <= pHighestAddressWhereCanReadOneVector); |
| | | 251 | | } |
| | | 252 | | } |
| | | 253 | | |
| | | 254 | | // Vectorization isn't supported on our current platform, or the input was too small to benefit |
| | | 255 | | // from vectorization, or we saw invalid UTF-16 data in the vectorized code paths and need to |
| | | 256 | | // drain remaining valid chars before we report failure. |
| | | 257 | | |
| | 0 | 258 | | for (; pInputBuffer < pEndOfInputBuffer; pInputBuffer++) |
| | | 259 | | { |
| | 0 | 260 | | uint thisChar = pInputBuffer[0]; |
| | 0 | 261 | | if (thisChar <= 0x7F) |
| | | 262 | | { |
| | | 263 | | continue; |
| | | 264 | | } |
| | | 265 | | |
| | | 266 | | // Bump adjustment by +1 for U+0080..U+07FF; by +2 for U+0800..U+FFFF. |
| | | 267 | | // This optimistically assumes no surrogates, which we'll handle shortly. |
| | | 268 | | |
| | 0 | 269 | | tempUtf8CodeUnitCountAdjustment += (thisChar + 0x0001_F800u) >> 16; |
| | | 270 | | |
| | 0 | 271 | | if (!UnicodeUtility.IsSurrogateCodePoint(thisChar)) |
| | | 272 | | { |
| | | 273 | | continue; |
| | | 274 | | } |
| | | 275 | | |
| | | 276 | | // Found a surrogate char. Back out the adjustment we made above, then |
| | | 277 | | // try to consume the entire surrogate pair all at once. We won't bother |
| | | 278 | | // trying to interpret the surrogate pair as a scalar value; we'll only |
| | | 279 | | // validate that its bit pattern matches what's expected for a surrogate pair. |
| | | 280 | | |
| | 0 | 281 | | tempUtf8CodeUnitCountAdjustment -= 2; |
| | | 282 | | |
| | 0 | 283 | | if ((nuint)pEndOfInputBuffer - (nuint)pInputBuffer < sizeof(uint)) |
| | | 284 | | { |
| | | 285 | | goto Error; // input buffer too small to read a surrogate pair |
| | | 286 | | } |
| | | 287 | | |
| | 0 | 288 | | thisChar = Unsafe.ReadUnaligned<uint>(pInputBuffer); |
| | 0 | 289 | | if (((thisChar - (BitConverter.IsLittleEndian ? 0xDC00_D800u : 0xD800_DC00u)) & 0xFC00_FC00u) != 0) |
| | | 290 | | { |
| | | 291 | | goto Error; // not a well-formed surrogate pair |
| | | 292 | | } |
| | | 293 | | |
| | 0 | 294 | | tempScalarCountAdjustment--; // 2 UTF-16 code units -> 1 scalar |
| | 0 | 295 | | tempUtf8CodeUnitCountAdjustment += 2; // 2 UTF-16 code units -> 4 UTF-8 code units |
| | | 296 | | |
| | 0 | 297 | | pInputBuffer++; // consumed one extra char |
| | | 298 | | } |
| | | 299 | | |
| | | 300 | | Error: |
| | | 301 | | |
| | | 302 | | // Also used for normal return. |
| | | 303 | | |
| | 0 | 304 | | utf8CodeUnitCountAdjustment = tempUtf8CodeUnitCountAdjustment; |
| | 0 | 305 | | scalarCountAdjustment = tempScalarCountAdjustment; |
| | 0 | 306 | | return pInputBuffer; |
| | | 307 | | } |
| | | 308 | | } |
| | | 309 | | } |
| | | 310 | | |