|
| 1 | +using System; |
| 2 | +using System.Buffers; |
| 3 | +using System.Text; |
| 4 | + |
| 5 | +namespace StackExchange.Redis; |
| 6 | + |
| 7 | +internal static class EncodingExtensions |
| 8 | +{ |
| 9 | + // Above this length we stream through a Decoder; at or below it we linearize onto the stack and use the |
| 10 | + // contiguous span overloads, avoiding the Decoder heap allocation for the common (small) case. |
| 11 | + private const int MaxStackLinearizeBytes = 128; |
| 12 | + |
| 13 | + // Note: there is no BCL Encoding.GetCharCount(in ReadOnlySequence<byte>) on any TFM (unlike GetChars, |
| 14 | + // which the BCL provides on modern runtimes), so we supply it for all targets. |
| 15 | + public static int GetCharCount(this Encoding encoding, in ReadOnlySequence<byte> seq) |
| 16 | + { |
| 17 | + // common case: a single segment can be measured directly, with no decoder state to track |
| 18 | + if (seq.IsSingleSegment) return encoding.GetCharCount(seq.FirstSpan); |
| 19 | + |
| 20 | + // small payloads: linearize onto the stack and measure contiguously - no Decoder allocation, and no |
| 21 | + // glyph-straddles-a-boundary problem once the bytes are contiguous |
| 22 | + long length = seq.Length; |
| 23 | + if (length <= MaxStackLinearizeBytes) |
| 24 | + { |
| 25 | + Span<byte> linear = stackalloc byte[(int)length]; |
| 26 | + seq.CopyTo(linear); |
| 27 | + return encoding.GetCharCount(linear); |
| 28 | + } |
| 29 | + |
| 30 | + // larger multi-segment: a multi-byte glyph can straddle a segment boundary, so we *must* decode with |
| 31 | + // a stateful decoder rather than summing per-segment counts (which would over-count split glyphs). |
| 32 | + // Note we cannot use Decoder.GetCharCount: unlike GetChars/Convert it does not carry partial-glyph |
| 33 | + // state between calls, so it too over-counts. Decoder.Convert reports the chars produced without us |
| 34 | + // having to keep them, so we decode into a small scratch buffer and discard the output, flushing on |
| 35 | + // the final segment. |
| 36 | + var decoder = encoding.GetDecoder(); |
| 37 | + Span<char> scratch = stackalloc char[128]; |
| 38 | + int count = 0; |
| 39 | + var position = seq.Start; |
| 40 | + bool have = seq.TryGet(ref position, out var current); |
| 41 | + while (have) |
| 42 | + { |
| 43 | + var nextPosition = position; |
| 44 | + bool haveNext = seq.TryGet(ref nextPosition, out var next); |
| 45 | + var bytes = current.Span; |
| 46 | + bool flush = !haveNext; |
| 47 | + bool completed; |
| 48 | + do |
| 49 | + { |
| 50 | + decoder.Convert(bytes, scratch, flush, out int bytesUsed, out int charsUsed, out completed); |
| 51 | + count += charsUsed; |
| 52 | + bytes = bytes.Slice(bytesUsed); |
| 53 | + } |
| 54 | + while (!bytes.IsEmpty || (flush && !completed)); |
| 55 | + current = next; |
| 56 | + position = nextPosition; |
| 57 | + have = haveNext; |
| 58 | + } |
| 59 | + return count; |
| 60 | + } |
| 61 | + |
| 62 | +#if NET461 || NET472 || NETSTANDARD2_0 |
| 63 | + // modern runtimes have a BCL Encoding.GetChars(in ReadOnlySequence<byte>, Span<char>); we only need to |
| 64 | + // supply it for the older targets. The flush-on-final-segment logic mirrors GetCharCount above, which |
| 65 | + // guarantees the two agree on the char count - so a buffer sized via GetCharCount cannot overflow here. |
| 66 | + public static int GetChars(this Encoding encoding, in ReadOnlySequence<byte> seq, Span<char> chars) |
| 67 | + { |
| 68 | + if (seq.IsSingleSegment) return encoding.GetChars(seq.FirstSpan, chars); |
| 69 | + |
| 70 | + // small payloads: linearize onto the stack and decode contiguously - no Decoder allocation |
| 71 | + long length = seq.Length; |
| 72 | + if (length <= MaxStackLinearizeBytes) |
| 73 | + { |
| 74 | + Span<byte> linear = stackalloc byte[(int)length]; |
| 75 | + seq.CopyTo(linear); |
| 76 | + return encoding.GetChars(linear, chars); |
| 77 | + } |
| 78 | + |
| 79 | + var decoder = encoding.GetDecoder(); |
| 80 | + int total = 0; |
| 81 | + var position = seq.Start; |
| 82 | + bool have = seq.TryGet(ref position, out var current); |
| 83 | + while (have) |
| 84 | + { |
| 85 | + var nextPosition = position; |
| 86 | + bool haveNext = seq.TryGet(ref nextPosition, out var next); |
| 87 | + int written = decoder.GetChars(current.Span, chars, flush: !haveNext); |
| 88 | + chars = chars.Slice(written); // advance by chars *written*, not by bytes consumed |
| 89 | + total += written; |
| 90 | + current = next; |
| 91 | + position = nextPosition; |
| 92 | + have = haveNext; |
| 93 | + } |
| 94 | + return total; |
| 95 | + } |
| 96 | +#endif |
| 97 | +} |
0 commit comments