-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTextValidation.cs
More file actions
190 lines (164 loc) · 6.14 KB
/
Copy pathTextValidation.cs
File metadata and controls
190 lines (164 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
using System.Buffers;
using System.Globalization;
using System.Text;
namespace LineEndingNormalizer;
/// <summary>
/// Provides strict decoding and text-quality validation for byte buffers
/// using a specified character encoding.
/// </summary>
internal static class TextValidation
{
//
// Minimum fraction of decoded Unicode scalars that must be text-like
// for the sample to be accepted as text.
//
// REMARKS:
// Calibrated on multilingual UTF-8/16/32 text, emoji, combining
// marks, supplementary characters, code/JSON/CSV, and random binary.
// All real-text samples scored 1.0; 0.9 provides a safety margin.
//
private const double MinPrintableFraction = 0.9;
/// <summary>
/// Strictly decodes the buffer using the specified encoding and checks
/// whether the decoded content looks like text.
/// A successful result establishes that the bytes are compatible with
/// the encoding and look like text, but does not prove that the encoding
/// is the original encoding. This distinction is especially important
/// for single-byte legacy encodings, which may accept the same bytes
/// under multiple encodings. The test is therefore strong for rejecting
/// incompatible encodings, but weak for uniquely identifying the
/// original encoding.
/// </summary>
internal static bool IsValidText(
Encoding encoding,
ReadOnlySpan<byte> buffer)
{
ArgumentNullException.ThrowIfNull(encoding);
if (buffer.IsEmpty)
return false;
int maxChars =
encoding.GetMaxCharCount(buffer.Length);
char[] chars =
ArrayPool<char>.Shared.Rent(maxChars);
try
{
if (!DecodeStrict(
encoding,
buffer,
chars,
out int charsWritten))
{
return false;
}
return LooksLikeText(
chars.AsSpan(0, charsWritten));
}
finally
{
ArrayPool<char>.Shared.Return(chars);
}
}
/// <summary>
/// Strictly decodes the specified bytes into a character buffer.
/// Invalid byte sequences are rejected. An incomplete trailing
/// sequence is accepted and omitted from the returned string.
/// </summary>
private static bool DecodeStrict(
Encoding encoding,
ReadOnlySpan<byte> buffer,
Span<char> chars,
out int charsWritten)
{
ArgumentNullException.ThrowIfNull(encoding);
charsWritten = 0;
if (buffer.IsEmpty)
return false;
//
// Force strict decoding regardless of the supplied Encoding instance.
//
// TextEncoding.Strict rebuilds the encoding with the fallbacks supplied up
// front. Assigning Decoder.Fallback afterwards is not enough on its own: for
// the CodePagesEncodingProvider encodings this method is asked to validate,
// the assignment is silently ignored and invalid bytes are substituted, so
// the decode below would succeed for input the encoding cannot represent.
//
Decoder decoder = TextEncoding.Strict(encoding).GetDecoder();
decoder.Fallback = DecoderFallback.ExceptionFallback;
try
{
//
// The buffer is a detection sample, not necessarily the complete file.
// Keep flush=false so an incomplete sequence at the sample boundary is
// not treated as invalid. Invalid sequences occurring within the sample
// still trigger DecoderFallbackException.
//
charsWritten = decoder.GetChars(
buffer,
chars,
flush: false);
return true;
}
catch (DecoderFallbackException)
{
return false;
}
}
/// <summary>
/// Determines whether decoded characters predominantly represent text.
/// Printable multilingual Unicode, emoji, combining marks, supplementary
/// characters, and common whitespace, are considered text.
/// </summary>
private static bool LooksLikeText(
ReadOnlySpan<char> text)
{
if (text.IsEmpty)
return false;
//
// Examine at most the first 500 Unicode scalar values.
//
int runeCount = 0;
int printable = 0;
foreach (Rune rune in text.EnumerateRunes())
{
if (runeCount >= 500)
break;
runeCount++;
//
// Common whitespace characters count as printable text.
//
if (rune.Value is '\r' or '\n' or '\t' or ' ')
{
printable++;
continue;
}
switch (Rune.GetUnicodeCategory(rune))
{
//
// Control and private-use characters are excluded from the printable
// ratio rather than rejecting the sample outright.
//
// Rejecting on the first private-use scalar made a whole file
// undetectable over one character - icon-font glyphs in markup are the
// common case - and did so inconsistently, since only the first 500
// scalars are examined, so the same character later in the file was
// accepted. Excluding them still rejects a buffer that is largely
// private-use, which is the binary evidence the check exists to find.
//
case UnicodeCategory.PrivateUse:
case UnicodeCategory.Control:
break;
//
// Treat all non-control, non-private-use scalars as text-like.
// This intentionally includes format, unassigned, separators,
// CJK, emoji, and combining marks.
//
default:
printable++;
break;
}
}
if (runeCount == 0)
return false;
return (double)printable / runeCount >= MinPrintableFraction;
}
}