@@ -22,6 +22,7 @@ public static class UserAgentHelpers
2222 TryParseScript ,
2323 TryParseTv ,
2424 TryParseConsole ,
25+ TryParseKnownDeviceCode ,
2526 TryParseIos ,
2627 TryParseAndroid ,
2728 TryParseChromeOs ,
@@ -30,7 +31,11 @@ public static class UserAgentHelpers
3031 TryParseServerFallback ,
3132 } ;
3233
33- private static readonly Regex _samsungModelRegex = new Regex ( @"\bSM-[A-Z]\d{3}[A-Z0-9]?\b" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
34+ private static readonly Regex _samsungModelRegex = new Regex ( @"\bSM-[A-Z]\d{3}[A-Z0-9]{0,4}\b" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
35+
36+ private static readonly Regex _appleMachineRegex = new Regex ( @"\b(iPhone|iPad)\d{1,2},\d{1,2}\b" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
37+ private static readonly Regex _onePlusRegex = new Regex ( @"\bCPH\d{4}\b" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
38+ private static readonly Regex _xiaomiSkuRegex = new Regex ( @"\b\d{5}[A-Z]{2}\d{2}[A-Z0-9]{1,3}\b" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
3439
3540 /// <summary>
3641 /// Determines the type of device based on the provided user agent string.
@@ -42,7 +47,7 @@ public static class UserAgentHelpers
4247 /// <param name="userAgent">The user agent string to analyze for device identification. Cannot be null or whitespace.</param>
4348 /// <returns>A string representing the detected device type, such as "iPhone", "Android Phone", "Windows PC", or "Bot".
4449 /// Returns "Unknown" if the user agent is null or whitespace.</returns>
45- public static string GetDevice ( string userAgent )
50+ public static string GetDevice ( string ? userAgent )
4651 {
4752 return GetDeviceInfo ( userAgent ) . FriendlyName ?? "Unknown" ;
4853 }
@@ -54,7 +59,7 @@ public static string GetDevice(string userAgent)
5459 /// <returns>A <see cref="UserAgentDeviceInfo"/> object containing the detected device information. If the device type
5560 /// cannot be determined, the returned object will have <see cref="UserAgentDeviceType.Unknown"/> as the device
5661 /// type.</returns>
57- public static UserAgentDeviceInfo GetDeviceInfo ( string userAgent )
62+ public static UserAgentDeviceInfo GetDeviceInfo ( string ? userAgent )
5863 {
5964 if ( string . IsNullOrWhiteSpace ( userAgent ) )
6065 {
@@ -73,6 +78,11 @@ public static UserAgentDeviceInfo GetDeviceInfo(string userAgent)
7378 return new UserAgentDeviceInfo ( UserAgentDeviceType . Unknown , null , "Unknown" ) ;
7479 }
7580
81+ private static UserAgentDeviceInfo ? TryParseKnownDeviceCode ( string ua )
82+ {
83+ return TryGetFirstKnownDeviceCode ( ua ) ;
84+ }
85+
7686 // small helpers
7787 private static bool ContainsAny ( string haystack , params string [ ] needles )
7888 {
@@ -96,7 +106,7 @@ private static bool ContainsAny(string haystack, params string[] needles)
96106 private static UserAgentDeviceInfo ? TryParseScript ( string ua )
97107 {
98108 var ual = ua . ToLowerInvariant ( ) ;
99- if ( ContainsAny ( ual , "curl/" , "wget/" , "httpclient" , "libwww" , "okhttp" , " java/") )
109+ if ( ContainsAny ( ual , "curl/" , "wget/" , "httpclient" , "libwww" , "java/" ) )
100110 {
101111 return new UserAgentDeviceInfo ( UserAgentDeviceType . Script , null , "Script" ) ;
102112 }
@@ -174,12 +184,35 @@ private static bool ContainsAny(string haystack, params string[] needles)
174184
175185 private static UserAgentDeviceInfo ? TryGetFirstKnownDeviceCode ( string value )
176186 {
177- // Currently we support Samsung-style model codes, but this can be expanded.
187+ // Lookup-only: we only return a match if the extracted code exists in KnownDeviceCodes.Map.
188+
189+ // 1) Samsung
178190 var samsungCode = GetFirstSamsungModelCode ( value ) ;
179191 if ( samsungCode != null && KnownDeviceCodes . Map . TryGetValue ( samsungCode , out var knownSamsung ) )
180192 {
181193 return knownSamsung ;
182194 }
195+
196+ // 2) Apple machine ids (useful in SDK/logs, typically not present in Safari UA)
197+ var am = _appleMachineRegex . Match ( value ) ;
198+ if ( am . Success && KnownDeviceCodes . Map . TryGetValue ( am . Value , out var knownApple ) )
199+ {
200+ return knownApple ;
201+ }
202+
203+ // 3) OnePlus CPH
204+ var op = _onePlusRegex . Match ( value ) ;
205+ if ( op . Success && KnownDeviceCodes . Map . TryGetValue ( op . Value , out var knownOp ) )
206+ {
207+ return knownOp ;
208+ }
209+
210+ // 4) Xiaomi-ish SKU (rough; match then lookup)
211+ var xi = _xiaomiSkuRegex . Match ( value ) ;
212+ if ( xi . Success && KnownDeviceCodes . Map . TryGetValue ( xi . Value , out var knownXi ) )
213+ {
214+ return knownXi ;
215+ }
183216 return null ;
184217 }
185218
@@ -192,7 +225,9 @@ private static UserAgentDeviceType ResolveSamsungType(string samsungModelCode)
192225 return UserAgentDeviceType . SamsungTablet ;
193226 }
194227
195- if ( samsungModelCode . StartsWith ( "SM-R" , StringComparison . OrdinalIgnoreCase ) || samsungModelCode . StartsWith ( "SM-W" , StringComparison . OrdinalIgnoreCase ) )
228+ if ( samsungModelCode . StartsWith ( "SM-R" , StringComparison . OrdinalIgnoreCase )
229+ || samsungModelCode . StartsWith ( "SM-W" , StringComparison . OrdinalIgnoreCase )
230+ || samsungModelCode . StartsWith ( "SM-L" , StringComparison . OrdinalIgnoreCase ) )
196231 {
197232 return UserAgentDeviceType . SamsungWatch ;
198233 }
@@ -221,17 +256,23 @@ private static UserAgentDeviceType ResolveSamsungType(string samsungModelCode)
221256 var token = parts [ i ] . Trim ( ) ;
222257 if ( ! token . StartsWith ( "Android" , StringComparison . OrdinalIgnoreCase ) ) continue ;
223258
224- var next = i + 1 < parts . Length ? parts [ i + 1 ] . Trim ( ) : null ;
225- if ( string . IsNullOrWhiteSpace ( next ) ) return null ;
226-
227- // ignore common noise tokens
228- if ( next . Equals ( "wv" , StringComparison . OrdinalIgnoreCase ) || next . Equals ( "mobile" , StringComparison . OrdinalIgnoreCase ) )
259+ for ( var j = i + 1 ; j < parts . Length ; j ++ )
229260 {
230- return null ;
261+ var cand = parts [ j ] . Trim ( ) ;
262+ if ( string . IsNullOrWhiteSpace ( cand ) ) continue ;
263+
264+ // ignore common noise tokens
265+ if ( cand . Equals ( "wv" , StringComparison . OrdinalIgnoreCase )
266+ || cand . Equals ( "mobile" , StringComparison . OrdinalIgnoreCase ) )
267+ {
268+ continue ;
269+ }
270+
271+ var model = SanitizeModel ( cand ) ;
272+ return model . Length > 0 ? model : null ;
231273 }
232274
233- var model = SanitizeModel ( next ) ;
234- return model . Length > 0 ? model : null ;
275+ return null ;
235276 }
236277 return null ;
237278 }
0 commit comments