< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.PortableText
Assembly: AsyncResponse.Core
File(s): /_/src/AsyncResponse.Core/PortableText.cs
Line coverage
100%
Covered lines: 29
Uncovered lines: 0
Coverable lines: 29
Total lines: 116
Line coverage: 100%
Branch coverage
100%
Covered branches: 24
Total branches: 24
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IndexOfIllFormedUtf16(...)100%1010100%
IndexOfControlCharacter(...)100%44100%
ControlCharacterRejection(...)100%11100%
Excerpt(...)100%22100%
TruncateWellFormed(...)100%88100%
IllFormedUtf16Rejection(...)100%11100%

File(s)

/_/src/AsyncResponse.Core/PortableText.cs

#LineLine coverage
 1namespace AsyncResponse;
 2
 3/// <summary>
 4/// Text rules shared by the two identifier contracts — correlation ids
 5/// (<see cref="AsyncResponseChannelOptions.CorrelationIdNotPortable"/>) and flow ids
 6/// (<c>FlowStateConcurrency.FlowIdNotPortable</c>). They are checked in one place because an
 7/// identifier crosses the same boundaries either way: it is encoded to UTF-8 for a subject, a key,
 8/// or a column, and compared ordinally by the engine on the way back.
 9/// </summary>
 10internal static class PortableText
 11{
 12    /// <summary>
 13    /// Finds the first ill-formed UTF-16 code unit — an unpaired surrogate — or <c>-1</c> when the
 14    /// string is well-formed.
 15    /// <para>
 16    /// A .NET <see cref="string"/> can hold one, and it is not merely exotic: every UTF-8 encoder
 17    /// in the framework defaults to REPLACING it with U+FFFD rather than failing. That silent
 18    /// substitution is what makes an unpaired surrogate dangerous here rather than merely invalid.
 19    /// Two ids the engine considers different — a lone <c>U+D800</c> and a literal <c>U+FFFD</c> —
 20    /// encode to identical bytes, so they collide on anything derived from those bytes: a NATS
 21    /// subject, a recovery key, a hash. One conversation's response then reaches the other's
 22    /// waiter, which is exactly the failure the ordinal-identity contract exists to prevent.
 23    /// </para>
 24    /// </summary>
 25    internal static int IndexOfIllFormedUtf16(string value)
 26    {
 156815027        for (var index = 0; index < value.Length; index++)
 28        {
 76375329            if (!char.IsSurrogate(value[index]))
 30                continue;
 31
 32            // A high surrogate followed by a low one is a well-formed pair: skip both.
 5233            if (char.IsHighSurrogate(value[index])
 5234                && index + 1 < value.Length
 5235                && char.IsLowSurrogate(value[index + 1]))
 36            {
 2237                index++;
 2238                continue;
 39            }
 40
 41            // Anything else is unpaired: a high surrogate at the end or before a non-low unit, or
 42            // a low surrogate with no high unit before it.
 3043            return index;
 44        }
 45
 2032246        return -1;
 47    }
 48
 49    /// <summary>
 50    /// Finds the first control character, or <c>-1</c> when there is none.
 51    /// <para>
 52    /// Control characters are not merely ugly in diagnostics: U+0000 in particular is rejected
 53    /// outright by PostgreSQL's <c>text</c> type (SQLSTATE 22021, "invalid byte sequence for
 54    /// encoding UTF8: 0x00") while SQL Server's <c>nvarchar</c> stores it happily, so an id
 55    /// carrying one exists on one store and fails at its first write on another — the opposite of
 56    /// portable, and diagnosed only as an opaque driver error far from the call site.
 57    /// </para>
 58    /// </summary>
 59    internal static int IndexOfControlCharacter(string value)
 60    {
 113506661        for (var index = 0; index < value.Length; index++)
 62        {
 55237463            if (char.IsControl(value[index]))
 1464                return index;
 65        }
 66
 1515967        return -1;
 68    }
 69
 70    /// <summary>The rejection message for a control character, worded for the given kind of id.</summary>
 71    internal static string ControlCharacterRejection(string kind, string excerpt, char offending, int index)
 1472        => $"{kind} '{excerpt}' contains the control character \\u{(int)offending:x4} at index {index}. Control characte
 1473            "portable: PostgreSQL rejects U+0000 in a text column outright (22021) while SQL Server stores it, so the sa
 1474            "succeeds on one store and fails at its first write on another, and control characters corrupt diagnostics "
 1475            "everywhere. Use a printable id.";
 76
 77    /// <summary>
 78    /// The shared 40-character excerpt used when quoting an offending id back to the caller. Cut
 79    /// through <see cref="TruncateWellFormed"/>: a fixed-index slice used to split a surrogate pair
 80    /// that straddled the cut, so the helper that quotes an id in the "unpaired surrogate"
 81    /// rejection could mint an unpaired surrogate of its own.
 82    /// </summary>
 83    internal static string Excerpt(string value)
 4084        => value.Length <= 40 ? value : string.Concat(TruncateWellFormed(value, 40), "…");
 85
 86    /// <summary>
 87    /// The longest prefix of <paramref name="value"/> that fits <paramref name="maxLength"/> UTF-16
 88    /// code units WITHOUT ending inside a surrogate pair — one unit shorter than the budget when
 89    /// the pair straddles it. <c>value[..maxLength]</c> keeps the high surrogate and drops its low
 90    /// half; every UTF-8 encoder then substitutes U+FFFD for the orphan (see
 91    /// <see cref="IndexOfIllFormedUtf16"/>), silently corrupting the text at the cut — or, under a
 92    /// strict encoder, failing the write that carried it. Every length-capped diagnostic string
 93    /// (id excerpts, dead-letter reasons, generated consumer names) is cut here. A surrogate that
 94    /// was already unpaired in the input is not repaired: this never makes text worse, it only
 95    /// refuses to break a pair that was whole.
 96    /// </summary>
 97    internal static string TruncateWellFormed(string value, int maxLength)
 98    {
 18699        ArgumentOutOfRangeException.ThrowIfNegative(maxLength);
 184100        if (value.Length <= maxLength)
 116101            return value;
 102
 68103        var cut = maxLength;
 68104        if (cut > 0 && char.IsHighSurrogate(value[cut - 1]) && char.IsLowSurrogate(value[cut]))
 14105            cut--;
 106
 68107        return value[..cut];
 108    }
 109
 110    /// <summary>The rejection message for an unpaired surrogate, worded for the given kind of id.</summary>
 111    internal static string IllFormedUtf16Rejection(string kind, string excerpt, char offending, int index)
 30112        => $"{kind} '{excerpt}' is not well-formed UTF-16: the code unit at index {index} (\\u{(int)offending:x4}) is an
 30113            "surrogate. Encoders substitute U+FFFD for it rather than failing, so this id and one containing a literal U
 30114            "produce identical bytes — and therefore the same NATS subject, recovery key, and stored value — while the e
 30115            "compares them ordinally and treats them as two different conversations. Send a well-formed id.";
 116}