@@ -27,22 +27,30 @@ public class ConformanceIdnaTestResult
27
27
/// </summary>
28
28
public string Value { get ; private set ; }
29
29
30
+ public string ? Source { get ; private set ; }
31
+
30
32
public IdnaTestResultType ResultType { get ; private set ; }
31
33
32
34
public string StatusValue { get ; private set ; }
33
35
34
36
public ConformanceIdnaTestResult ( string entry , string fallbackValue , IdnaTestResultType resultType = IdnaTestResultType . ToAscii )
35
- : this ( entry , fallbackValue , null , null , useValueForStatus : true , resultType )
37
+ : this ( entry , fallbackValue , null , null , useValueForStatus : true , resultType , null )
36
38
{
37
39
}
38
40
39
41
public ConformanceIdnaTestResult ( string entry , string fallbackValue , string statusValue , string statusFallbackValue , IdnaTestResultType resultType = IdnaTestResultType . ToAscii )
40
- : this ( entry , fallbackValue , statusValue , statusFallbackValue , useValueForStatus : false , resultType )
42
+ : this ( entry , fallbackValue , statusValue , statusFallbackValue , useValueForStatus : false , resultType , null )
43
+ {
44
+ }
45
+
46
+ public ConformanceIdnaTestResult ( string entry , string fallbackValue , string statusValue , string statusFallbackValue , string ? source , IdnaTestResultType resultType = IdnaTestResultType . ToAscii )
47
+ : this ( entry , fallbackValue , statusValue , statusFallbackValue , useValueForStatus : false , resultType , source )
41
48
{
42
49
}
43
50
44
- private ConformanceIdnaTestResult ( string entry , string fallbackValue , string statusValue , string statusFallbackValue , bool useValueForStatus , IdnaTestResultType resultType )
51
+ private ConformanceIdnaTestResult ( string entry , string fallbackValue , string statusValue , string statusFallbackValue , bool useValueForStatus , IdnaTestResultType resultType , string ? source )
45
52
{
53
+ Source = source ;
46
54
ResultType = resultType ;
47
55
SetValue ( string . IsNullOrEmpty ( entry . Trim ( ) ) ? fallbackValue : entry ) ;
48
56
SetSuccess ( useValueForStatus ?
@@ -81,11 +89,120 @@ private void SetSuccess(string statusValue)
81
89
}
82
90
}
83
91
92
+ // Fullwidth Full Stop, Ideographic Full Stop, and Halfwidth Ideographic Full Stop
93
+ private static char [ ] AllDots = [ '.' , '\uFF0E ' , '\u3002 ' , '\uFF61 ' ] ;
94
+
95
+ private const char SoftHyphen = '\u00AD ' ;
96
+
97
+ private bool IsIgnorableA4_2Rule ( )
98
+ {
99
+ if ( Source is null )
100
+ {
101
+ return false ;
102
+ }
103
+
104
+ // Check the label lengths for the ASCII
105
+ int lastIndex = 0 ;
106
+ int index = Value . IndexOfAny ( AllDots ) ;
107
+ while ( index >= 0 )
108
+ {
109
+ if ( index - lastIndex > 63 ) // 63 max label length
110
+ {
111
+ return false ;
112
+ }
113
+
114
+ lastIndex = index + 1 ;
115
+ index = Value . IndexOfAny ( AllDots , lastIndex ) ;
116
+ }
117
+
118
+ if ( Value . Length - lastIndex > 63 )
119
+ {
120
+ return false ;
121
+ }
122
+
123
+ // Remove Hyphen as it is ignored
124
+ if ( Source . IndexOf ( SoftHyphen ) >= 0 )
125
+ {
126
+ Span < char > span = stackalloc char [ Source . Length ] ;
127
+ int spanIndex = 0 ;
128
+
129
+ for ( int i = 0 ; i < Source . Length ; i ++ )
130
+ {
131
+ if ( Source [ i ] != SoftHyphen )
132
+ {
133
+ span [ spanIndex ++ ] = Source [ i ] ;
134
+ }
135
+ }
136
+
137
+ Source = span . Slice ( 0 , spanIndex ) . ToString ( ) ;
138
+ }
139
+
140
+ // Check the label lengths for the Source
141
+ lastIndex = 0 ;
142
+ index = Source . IndexOfAny ( AllDots ) ;
143
+ while ( index >= 0 )
144
+ {
145
+ if ( index - lastIndex > 63 ) // 63 max label length
146
+ {
147
+ return false ;
148
+ }
149
+
150
+ lastIndex = index + 1 ;
151
+ index = Source . IndexOfAny ( AllDots , lastIndex ) ;
152
+ }
153
+
154
+ if ( Source . Length - lastIndex > 63 )
155
+ {
156
+ return false ;
157
+ }
158
+
159
+ if ( Source [ 0 ] is '.' ) // Leading dot
160
+ {
161
+ return false ;
162
+ }
163
+
164
+ for ( int i = 0 ; i < Source . Length - 1 ; i ++ )
165
+ {
166
+ // Consequence dots
167
+ if ( ( Source [ i ] is '.' or '\uFF0E ' or '\u3002 ' or '\uFF61 ' ) && ( Source [ i + 1 ] is '.' or '\uFF0E ' or '\u3002 ' or '\uFF61 ' ) )
168
+ {
169
+ return false ;
170
+ }
171
+
172
+ // Check Historical Ranges
173
+ if ( Source [ i ] >= 0x2C00 && Source [ i ] <= 0x2C5F ) // Glagolitic (U+2C00–U+2C5F)
174
+ return false ;
175
+
176
+ switch ( Source [ i ] )
177
+ {
178
+ case '\uD800 ' :
179
+ if ( Source [ i + 1 ] >= 0xDFA0 && Source [ i + 1 ] <= 0xDFDF ) return false ; // Old Persian (U+103A0–U+103DF)
180
+ if ( Source [ i + 1 ] >= 0xDF30 && Source [ i + 1 ] <= 0xDF4F ) return false ; // Gothic (U+10330–U+1034F)
181
+ if ( Source [ i + 1 ] >= 0xDC00 && Source [ i + 1 ] <= 0xDC7F ) return false ; // Linear B (U+10000–U+1007F)
182
+ break ;
183
+ case '\uD802 ' :
184
+ if ( Source [ i + 1 ] >= 0xDD00 && Source [ i + 1 ] <= 0xDD1F ) return false ; // Phoenician (U+10900–U+1091F)
185
+ break ;
186
+ case '\uD803 ' :
187
+ if ( Source [ i + 1 ] >= 0xDEA0 && Source [ i + 1 ] <= 0xDEAF ) return false ; // Elymaic (U+10EA0–U+10EAF)
188
+ break ;
189
+ case '\uD808 ' :
190
+ if ( Source [ i + 1 ] >= 0xDC00 && Source [ i + 1 ] <= 0xDFFF ) return false ; // Cuneiform (U+12000–U+123FF)
191
+ break ;
192
+ case '\uD838 ' :
193
+ if ( Source [ i + 1 ] >= 0xDC00 && Source [ i + 1 ] <= 0xDCDF ) return false ; // Indic Siyaq Numbers (U+1E800–U+1E8DF)
194
+ break ;
195
+ }
196
+ }
197
+
198
+ return true ;
199
+ }
200
+
84
201
private bool IsIgnoredError ( string statusCode )
85
202
{
86
203
// We don't validate for BIDI rule so we can ignore BIDI codes
87
204
// If we're validating ToAscii we ignore rule V2 (UIDNA_ERROR_HYPHEN_3_4) for compatibility with windows.
88
- return statusCode . StartsWith ( 'B' ) || ( ResultType == IdnaTestResultType . ToAscii && statusCode == "V2" ) ;
205
+ return statusCode . StartsWith ( 'B' ) || ( ResultType == IdnaTestResultType . ToAscii && statusCode == "V2" ) || ( statusCode . StartsWith ( "A4_2" ) && IsIgnorableA4_2Rule ( ) ) ;
89
206
}
90
207
}
91
208
}
0 commit comments