@@ -18,7 +18,6 @@ public static class HttpUserAgentParser
1818 /// </summary>
1919 public static HttpUserAgentInformation Parse ( string userAgent )
2020 {
21- // prepare
2221 userAgent = Cleanup ( userAgent ) ;
2322
2423 // analyze
@@ -78,6 +77,11 @@ public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out Http
7877 public static ( string Name , string ? Version ) ? GetBrowser ( string userAgent )
7978 {
8079 ReadOnlySpan < char > ua = userAgent . AsSpan ( ) ;
80+ // Require a realistic browser UA prefix to avoid classifying truncated tokens as browsers
81+ if ( ! ContainsIgnoreCase ( ua , "Mozilla/" ) )
82+ {
83+ return null ;
84+ }
8185 foreach ( ( string Name , string DetectToken , string ? VersionToken ) browserRule in HttpUserAgentStatics . s_browserRules )
8286 {
8387 if ( ! TryIndexOf ( ua , browserRule . DetectToken , out int detectIndex ) )
@@ -86,7 +90,19 @@ public static (string Name, string? Version)? GetBrowser(string userAgent)
8690 }
8791
8892 // Version token may differ (e.g., Safari uses "Version/")
89- int versionSearchStart = detectIndex ;
93+ // Keep full span immutable across iterations
94+ ReadOnlySpan < char > uaFull = userAgent . AsSpan ( ) ;
95+ int versionSearchStart ;
96+ // For rules without a specific version token, ensure pattern Token/<digits>
97+ if ( string . IsNullOrEmpty ( browserRule . VersionToken ) )
98+ {
99+ int afterDetect = detectIndex + browserRule . DetectToken . Length ;
100+ if ( afterDetect >= uaFull . Length || uaFull [ afterDetect ] != '/' )
101+ {
102+ // Likely a misspelling or partial token (e.g., Edgg, Oprea, Chromee)
103+ continue ;
104+ }
105+ }
90106 if ( ! string . IsNullOrEmpty ( browserRule . VersionToken ) )
91107 {
92108 if ( TryIndexOf ( ua , browserRule . VersionToken ! , out int vtIndex ) )
@@ -104,14 +120,22 @@ public static (string Name, string? Version)? GetBrowser(string userAgent)
104120 versionSearchStart = detectIndex + browserRule . DetectToken . Length ;
105121 }
106122
107- string ? version = null ;
108- ua = ua . Slice ( versionSearchStart ) ;
109- if ( TryExtractVersion ( ua , out Range range ) )
123+ // Work on a local slice to avoid mutating the main span for following rules
124+ if ( versionSearchStart < 0 || versionSearchStart >= uaFull . Length )
125+ {
126+ // Nothing to search; try next rule
127+ continue ;
128+ }
129+
130+ ReadOnlySpan < char > search = uaFull . Slice ( versionSearchStart ) ;
131+ if ( TryExtractVersion ( search , out Range range ) )
110132 {
111- version = ua [ range ] . ToString ( ) ;
133+ string ? version = search [ range ] . ToString ( ) ;
134+ return ( browserRule . Name , version ) ;
112135 }
113136
114- return ( browserRule . Name , version ) ;
137+ // If we didn't find a version for this rule, try next rule
138+ continue ;
115139 }
116140
117141 return null ;
@@ -198,39 +222,43 @@ private static bool TryExtractVersion(ReadOnlySpan<char> haystack, out Range ran
198222
199223 // Limit search window to avoid scanning entire UA string unnecessarily
200224 const int Window = 128 ;
201- if ( haystack . Length >= Window )
225+ if ( haystack . Length > Window )
202226 {
203227 haystack = haystack . Slice ( 0 , Window ) ;
204228 }
205229
206- int i = 0 ;
207- for ( ; i < haystack . Length ; ++ i )
230+ // Find first digit
231+ int start = - 1 ;
232+ for ( int i = 0 ; i < haystack . Length ; i ++ )
208233 {
209234 char c = haystack [ i ] ;
210- if ( char . IsBetween ( c , '0' , '9' ) )
235+ if ( c >= '0' && c <= '9' )
211236 {
237+ start = i ;
212238 break ;
213239 }
214240 }
215241
216- int s = i ;
217- haystack = haystack . Slice ( i + 1 ) ;
218- for ( i = 0 ; i < haystack . Length ; ++ i )
242+ if ( start < 0 )
219243 {
220- char c = haystack [ i ] ;
221- if ( ! ( char . IsBetween ( c , '0' , '9' ) || c == '.' ) )
222- {
223- break ;
224- }
244+ // No digit found => no version
245+ return false ;
225246 }
226- i += s + 1 ; // shift back the previous domain
227247
228- if ( i == s )
248+ // Consume digits and dots after first digit
249+ int end = start + 1 ;
250+ while ( end < haystack . Length )
229251 {
230- return false ;
252+ char c = haystack [ end ] ;
253+ if ( ! ( ( c >= '0' && c <= '9' ) || c == '.' ) )
254+ {
255+ break ;
256+ }
257+ end ++ ;
231258 }
232259
233- range = new Range ( s , i ) ;
260+ // Create exclusive end range
261+ range = new Range ( start , end ) ;
234262 return true ;
235263 }
236264}
0 commit comments