| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Text; |
| | | 3 | | |
| | | 4 | | namespace AsyncResponse; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Quotes UNTRUSTED text — an inbound correlation id, a persisted type or method name — into a log |
| | | 8 | | /// line, an exception message, or an activity status without handing its author the line. |
| | | 9 | | /// <para> |
| | | 10 | | /// The ids and names quoted here are rejected precisely because they are malformed, and the |
| | | 11 | | /// malformation is frequently the payload: a CR/LF pair inside the quoted excerpt ends the real |
| | | 12 | | /// log entry and starts a forged one in every line-oriented sink, and an unpaired surrogate is |
| | | 13 | | /// replaced with U+FFFD by the sink's UTF-8 encoder, so the persisted line no longer says what |
| | | 14 | | /// was received. The excerpt therefore carries such code units as visible backslash-u escapes, |
| | | 15 | | /// never raw. |
| | | 16 | | /// </para> |
| | | 17 | | /// </summary> |
| | | 18 | | internal static class DiagnosticText |
| | | 19 | | { |
| | | 20 | | private const char LineSeparator = (char)0x2028; |
| | | 21 | | private const char ParagraphSeparator = (char)0x2029; |
| | | 22 | | |
| | | 23 | | /// <summary> |
| | | 24 | | /// The first <paramref name="maxLength"/> UTF-16 code units of <paramref name="value"/> (cut |
| | | 25 | | /// through <see cref="PortableText.TruncateWellFormed"/>, so a whole surrogate pair is never |
| | | 26 | | /// split), with every control character, line/paragraph separator, and unpaired surrogate |
| | | 27 | | /// written as a backslash-u escape, and an ellipsis appended when anything was cut. The budget |
| | | 28 | | /// applies to the INPUT: an escape widens one unit to six, so the result is at most six times |
| | | 29 | | /// the budget. |
| | | 30 | | /// </summary> |
| | | 31 | | internal static string EscapedExcerpt(string value, int maxLength = 40) |
| | | 32 | | { |
| | 191 | 33 | | var truncated = value.Length > maxLength; |
| | 191 | 34 | | var text = truncated ? PortableText.TruncateWellFormed(value, maxLength) : value; |
| | | 35 | | |
| | 191 | 36 | | var firstUnsafe = IndexOfUnsafe(text); |
| | 191 | 37 | | if (firstUnsafe < 0) |
| | 167 | 38 | | return truncated ? string.Concat(text, "…") : text; |
| | | 39 | | |
| | 24 | 40 | | var builder = new StringBuilder(text.Length + 16); |
| | 24 | 41 | | builder.Append(text, 0, firstUnsafe); |
| | 156 | 42 | | for (var index = firstUnsafe; index < text.Length; index++) |
| | | 43 | | { |
| | 54 | 44 | | var unit = text[index]; |
| | 54 | 45 | | if (IsPairStart(text, index)) |
| | | 46 | | { |
| | | 47 | | // A well-formed pair is ordinary text: keep both halves. |
| | 0 | 48 | | builder.Append(unit).Append(text[++index]); |
| | 0 | 49 | | continue; |
| | | 50 | | } |
| | | 51 | | |
| | 54 | 52 | | if (IsUnsafe(unit)) |
| | 24 | 53 | | builder.Append('\\').Append('u').Append(((int)unit).ToString("x4", CultureInfo.InvariantCulture)); |
| | | 54 | | else |
| | 30 | 55 | | builder.Append(unit); |
| | | 56 | | } |
| | | 57 | | |
| | 24 | 58 | | if (truncated) |
| | 0 | 59 | | builder.Append('…'); |
| | | 60 | | |
| | 24 | 61 | | return builder.ToString(); |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | private static int IndexOfUnsafe(string text) |
| | | 65 | | { |
| | 13060 | 66 | | for (var index = 0; index < text.Length; index++) |
| | | 67 | | { |
| | 6363 | 68 | | if (IsPairStart(text, index)) |
| | | 69 | | { |
| | 0 | 70 | | index++; |
| | 0 | 71 | | continue; |
| | | 72 | | } |
| | | 73 | | |
| | 6363 | 74 | | if (IsUnsafe(text[index])) |
| | 24 | 75 | | return index; |
| | | 76 | | } |
| | | 77 | | |
| | 167 | 78 | | return -1; |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | private static bool IsPairStart(string text, int index) |
| | 6417 | 82 | | => char.IsHighSurrogate(text[index]) && index + 1 < text.Length && char.IsLowSurrogate(text[index + 1]); |
| | | 83 | | |
| | | 84 | | // Reached only for a unit that is NOT part of a well-formed pair, so any surrogate seen here |
| | | 85 | | // is unpaired. The line and paragraph separators are not control characters to char.IsControl, |
| | | 86 | | // but they are line breaks to the viewers and parsers that matter for log forging. |
| | | 87 | | private static bool IsUnsafe(char unit) |
| | 6417 | 88 | | => char.IsControl(unit) || char.IsSurrogate(unit) || unit is LineSeparator or ParagraphSeparator; |
| | | 89 | | } |