diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..62ffd87 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,129 @@ +# GitHub Copilot Instructions + +## 1. Repository Context & Mission + +**Repository:** `HttpUserAgentParser` (mycsharp) + +**Primary Goal:** +Provide a **high-performance, stable, and broadly compatible .NET library** for parsing HTTP User-Agent strings, including integrations for ASP.NET Core and MemoryCache. + +**Core Design Principles:** +- API stability over convenience +- Predictable performance characteristics +- Minimal allocations in hot paths +- Full test coverage for all observable behavior + +--- + +## 2. Repository Structure (Authoritative) + +Copilot must understand and respect the architectural boundaries: + +- `src/HttpUserAgentParser` + → Core parsing logic and public APIs + +- `src/HttpUserAgentParser.AspNetCore` + → ASP.NET Core integration (middleware, extensions) + +- `src/HttpUserAgentParser.MemoryCache` + → Caching extensions and cache-aware abstractions + +- `tests/*` + → Unit tests for **all** shipped packages + → Tests define expected behavior and are the source of truth + +- `perf/*` + → Benchmarks for performance-sensitive code paths + +--- + +## 3. Standard .NET CLI Commands + +Use these commands consistently: + +- Clean: + `dotnet clean --nologo` + +- Restore: + `dotnet restore` + +- Build: + `dotnet build --nologo` + +- Test (all): + `dotnet test --nologo` + +- Test (single project): + `dotnet test --nologo` + +--- + +## 4. Autonomous Execution Rules (Critical) + +Copilot is expected to work **independently and end-to-end** without human intervention. + +### Mandatory Quality Gates (Never Skip) +- The solution **must compile** after every change +- **All tests must pass** +- New behavior **must include unit tests** +- Public APIs **must not break** existing users unless explicitly intended +- Changes must be **minimal, focused, and intentional** + +If any gate fails: +1. Diagnose the root cause +2. Fix the issue +3. Re-run the full validation cycle + +--- + +## 5. Change Strategy & Scope Control + +When solving a task, Copilot should: + +1. **Analyze existing code first** + - Prefer extension over modification + - Reuse established patterns and helpers +2. **Avoid architectural rewrites** + - No refactors unless explicitly required +3. **Preserve backward compatibility** + - No breaking changes to public APIs + - No silent behavioral changes + +If multiple solutions are possible: +- Prefer the **simplest**, **most explicit**, and **least invasive** option + +--- + +## 6. Testing Requirements + +Every functional change must be fully tested: + +- Unit tests are mandatory for: + - New features + - Bug fixes + - Edge cases and regressions +- Prefer existing utilities from: + `tests/HttpUserAgentParser.TestHelpers` + +Tests define correctness. If behavior is unclear, tests take precedence over assumptions. + +--- + +## 7. Performance Guidelines + +- Treat parsing logic as performance-critical +- Avoid unnecessary allocations and LINQ in hot paths +- Prefer spans, pooling, and cached results where appropriate +- Update or add benchmarks in `perf/*` for performance-relevant changes + +--- + +## 8. Output Expectations + +Copilot should deliver: +- Compilable, production-ready code +- Complete test coverage for new behavior +- Clear, intentional commits without unrelated changes + +**Do not stop early.** +A task is only complete when **all quality gates pass** and the solution is fully validated. diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5ddca9a..9b69370 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,13 +1,45 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "test", - "type": "shell", - "command": "dotnet test --nologo", - "args": [], - "problemMatcher": [ - "$msCompile" - ], - "group": "build" - } + "version": "2.0.0", + "tasks": [ + { + "label": "clean", + "type": "shell", + "command": "dotnet clean", + "problemMatcher": "$msCompile" + }, + { + "label": "restore", + "type": "shell", + "command": "dotnet restore", + "problemMatcher": "$msCompile" + }, + { + "label": "build", + "type": "shell", + "command": "dotnet build --nologo", + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "test", + "type": "shell", + "command": "dotnet test --nologo", + "problemMatcher": "$msCompile", + "group": "test" + }, + { + "label": "ci:validate", + "dependsOn": [ + "clean", + "restore", + "build", + "test" + ], + "dependsOrder": "sequence", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/README.md b/README.md index 599a556..215a944 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Parsing HTTP User Agents with .NET ## NuGet -| NuGet | -|-| -| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) | -| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` | +| NuGet | Install | +|-|-| +| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) | `dotnet add package MyCSharp.HttpUserAgentParser` | +| [![MyCSharp.HttpUserAgentParser.MemoryCache](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache) | `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCache` | | [![MyCSharp.HttpUserAgentParser.AspNetCore](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.AspNetCore.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.AspNetCore)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) | `dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore` | @@ -104,9 +104,9 @@ public void ConfigureServices(IServiceCollection services) Now you can use ```csharp -public void MyMethod(IHttpUserAgentParserAccessor parserAccessor) +public void MyMethod(IHttpUserAgentParserAccessor parserAccessor, HttpContext httpContext) { - HttpUserAgentInformation info = parserAccessor.Get(); + HttpUserAgentInformation? info = parserAccessor.Get(httpContext); } ``` @@ -152,7 +152,7 @@ by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.c MIT License -Copyright (c) 2021-2025 MyCSharp +Copyright (c) 2021-2026 MyCSharp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs b/src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs index b73b694..a3aa4cf 100644 --- a/src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs +++ b/src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs @@ -7,14 +7,26 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection; /// -/// Dependency injection extensions for ASP.NET Core environments +/// Extension methods for to add ASP.NET Core integration. /// public static class HttpUserAgentParserDependencyInjectionOptionsExtensions { /// - /// Registers as . - /// Requires a registered + /// Registers as a singleton implementation of . /// + /// The options instance from the parser registration. + /// The same options instance for method chaining. + /// + /// Requires a registered . + /// Call this after AddHttpUserAgentParser() or AddHttpUserAgentCachedParser(). + /// + /// + /// + /// IServiceCollection services = new ServiceCollection(); + /// services.AddHttpUserAgentParser() + /// .AddHttpUserAgentParserAccessor(); + /// + /// public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParserAccessor( this HttpUserAgentParserDependencyInjectionOptions options) { diff --git a/src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs b/src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs index 9da3b29..135bd4a 100644 --- a/src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs +++ b/src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs @@ -6,13 +6,24 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore; /// -/// Static extensions for +/// Extension methods for to access User-Agent information. /// public static class HttpContextExtensions { /// - /// Returns the User-Agent header value + /// Gets the User-Agent header value from the HTTP request. /// + /// The HTTP context. + /// The User-Agent string, or if not present. + /// + /// + /// string? userAgent = httpContext.GetUserAgentString(); + /// if (userAgent != null) + /// { + /// HttpUserAgentInformation info = HttpUserAgentParser.Parse(userAgent); + /// } + /// + /// public static string? GetUserAgentString(this HttpContext httpContext) { if (httpContext.Request.Headers.TryGetValue("User-Agent", out StringValues value)) diff --git a/src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs b/src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs index c5f87ed..2a33c52 100644 --- a/src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs +++ b/src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs @@ -6,25 +6,37 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore; /// -/// User Agent parser accessor. Implements +/// Default implementation of for ASP.NET Core applications. /// /// -/// Creates a new instance of +/// Extracts and parses the User-Agent header from HTTP requests. +/// Register via services.AddHttpUserAgentParser().AddHttpUserAgentParserAccessor(). /// +/// The parser provider to use for parsing. public class HttpUserAgentParserAccessor(IHttpUserAgentParserProvider httpUserAgentParser) : IHttpUserAgentParserAccessor { private readonly IHttpUserAgentParserProvider _httpUserAgentParser = httpUserAgentParser; - /// - /// User agent of current - /// + /// + /// + /// + /// string? userAgent = accessor.GetHttpContextUserAgent(httpContext); + /// + /// public string? GetHttpContextUserAgent(HttpContext httpContext) => httpContext.GetUserAgentString(); - /// - /// Returns current of current - /// + /// + /// + /// + /// HttpUserAgentInformation? info = accessor.Get(httpContext); + /// if (info != null) + /// { + /// Console.WriteLine(info.Value.Name); + /// } + /// + /// public HttpUserAgentInformation? Get(HttpContext httpContext) { string? httpUserAgent = GetHttpContextUserAgent(httpContext); diff --git a/src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs b/src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs index f33d5b4..de0a148 100644 --- a/src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs +++ b/src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs @@ -5,17 +5,24 @@ namespace MyCSharp.HttpUserAgentParser.AspNetCore; /// -/// User Agent parser accessor +/// Provides access to User-Agent parsing functionality within an ASP.NET Core context. /// public interface IHttpUserAgentParserAccessor { /// - /// User agent value + /// Gets the User-Agent header value from the specified HTTP context. /// + /// The HTTP context to extract the User-Agent from. + /// The User-Agent string, or if not present. string? GetHttpContextUserAgent(HttpContext httpContext); /// - /// Returns current + /// Parses the User-Agent from the specified HTTP context. /// + /// The HTTP context to extract and parse the User-Agent from. + /// + /// An instance if the User-Agent header is present; + /// otherwise, . + /// HttpUserAgentInformation? Get(HttpContext httpContext); } diff --git a/src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs b/src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs index cbe8534..dbb939c 100644 --- a/src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs +++ b/src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs @@ -7,13 +7,30 @@ namespace MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection; /// -/// Dependency injection extensions for IMemoryCache +/// Extension methods for registering with dependency injection. /// public static class HttpUserAgentParserMemoryCacheServiceCollectionExtensions { /// - /// Registers as singleton to + /// Registers as a singleton implementation of . /// + /// The service collection to add the services to. + /// Optional action to configure the cache options. + /// Options for further configuration. + /// + /// Default configuration: 256 entries maximum, 1 day sliding expiration. + /// Use the parameter to customize cache behavior. + /// + /// + /// + /// IServiceCollection services = new ServiceCollection(); + /// services.AddHttpUserAgentMemoryCachedParser(opts => + /// { + /// opts.CacheOptions.SizeLimit = 512; + /// opts.CacheEntryOptions.SlidingExpiration = TimeSpan.FromHours(6); + /// }); + /// + /// public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentMemoryCachedParser( this IServiceCollection services, Action? options = null) { diff --git a/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs b/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs index c8ecb91..9a8463d 100644 --- a/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs +++ b/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs @@ -5,11 +5,14 @@ namespace MyCSharp.HttpUserAgentParser.MemoryCache; -/// /// -/// Creates a new instance of . +/// Implementation of with caching using . /// -/// The options used to set expiration and size limit +/// +/// Provides sliding expiration and size limits for cached entries. +/// Default configuration: 256 entries maximum, 1 day sliding expiration. +/// +/// The options controlling cache size and expiration. public class HttpUserAgentParserMemoryCachedProvider( HttpUserAgentParserMemoryCachedProviderOptions options) : IHttpUserAgentParserProvider { @@ -17,6 +20,13 @@ public class HttpUserAgentParserMemoryCachedProvider( private readonly HttpUserAgentParserMemoryCachedProviderOptions _options = options; /// + /// + /// + /// HttpUserAgentParserMemoryCachedProviderOptions options = new(); + /// HttpUserAgentParserMemoryCachedProvider provider = new(options); + /// HttpUserAgentInformation info = provider.Parse("Mozilla/5.0 Chrome/90.0.4430.212"); + /// + /// public HttpUserAgentInformation Parse(string userAgent) { CacheKey key = GetKey(userAgent); diff --git a/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs b/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs index d9afb4a..e2cd81c 100644 --- a/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs +++ b/src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs @@ -5,39 +5,59 @@ namespace MyCSharp.HttpUserAgentParser.MemoryCache; /// -/// Provider options for +/// Configuration options for . +/// /// -/// Default of is 256. -/// Default of is 1 day +/// Default : 256 entries. +/// Default : 1 day. /// -/// public class HttpUserAgentParserMemoryCachedProviderOptions { /// - /// Cache options + /// Gets the memory cache configuration options. /// + /// Controls size limits and compaction behavior. public MemoryCacheOptions CacheOptions { get; } /// - /// Cache entry options + /// Gets the options applied to each cache entry. /// + /// Controls sliding expiration for cached user agent information. public MemoryCacheEntryOptions CacheEntryOptions { get; } /// /// Creates a new instance of /// + /// + /// + /// var options = new HttpUserAgentParserMemoryCachedProviderOptions(new MemoryCacheOptions { SizeLimit = 512 }); + /// + /// public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheOptions cacheOptions) : this(cacheOptions, null) { } /// /// Creates a new instance of /// + /// + /// + /// var options = new HttpUserAgentParserMemoryCachedProviderOptions( + /// new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(6) }); + /// + /// public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheEntryOptions cacheEntryOptions) : this(null, cacheEntryOptions) { } /// /// Creates a new instance of /// + /// + /// + /// var options = new HttpUserAgentParserMemoryCachedProviderOptions( + /// new MemoryCacheOptions { SizeLimit = 512 }, + /// new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(6) }); + /// + /// public HttpUserAgentParserMemoryCachedProviderOptions( MemoryCacheOptions? cacheOptions = null, MemoryCacheEntryOptions? cacheEntryOptions = null) { diff --git a/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs b/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs index 5804145..de41b1e 100644 --- a/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs +++ b/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs @@ -5,16 +5,18 @@ namespace MyCSharp.HttpUserAgentParser.DependencyInjection; /// -/// Options for dependency injection +/// Configuration options returned by the dependency injection registration methods. +/// Used for fluent configuration of additional features. /// /// -/// Creates a new instance of +/// This class provides access to the service collection for registering additional services +/// such as telemetry or ASP.NET Core integrations. /// -/// +/// The service collection to configure. public class HttpUserAgentParserDependencyInjectionOptions(IServiceCollection services) { /// - /// Services container + /// Gets the service collection being configured. /// public IServiceCollection Services { get; } = services; } diff --git a/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs b/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs index 23b96bc..a0c2d3e 100644 --- a/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs +++ b/src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs @@ -11,8 +11,16 @@ namespace MyCSharp.HttpUserAgentParser.DependencyInjection; public static class HttpUserAgentParserServiceCollectionExtensions { /// - /// Registers as singleton to + /// Registers as a singleton implementation of . /// + /// The service collection to add the services to. + /// Options for further configuration. + /// + /// + /// IServiceCollection services = new ServiceCollection(); + /// HttpUserAgentParserDependencyInjectionOptions options = services.AddHttpUserAgentParser(); + /// + /// public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( this IServiceCollection services) { @@ -20,8 +28,20 @@ public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentPars } /// - /// Registers as singleton to + /// Registers as a singleton implementation of . /// + /// The service collection to add the services to. + /// Options for further configuration. + /// + /// This provider caches parsed results indefinitely using a . + /// For expiration-based caching, use the MemoryCache package instead. + /// + /// + /// + /// IServiceCollection services = new ServiceCollection(); + /// HttpUserAgentParserDependencyInjectionOptions options = services.AddHttpUserAgentCachedParser(); + /// + /// public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentCachedParser( this IServiceCollection services) { @@ -29,8 +49,18 @@ public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentCach } /// - /// Registers as singleton to + /// Registers a custom implementation as a singleton. /// + /// The provider type implementing . + /// The service collection to add the services to. + /// Options for further configuration. + /// + /// + /// IServiceCollection services = new ServiceCollection(); + /// HttpUserAgentParserDependencyInjectionOptions options = services + /// .AddHttpUserAgentParser<HttpUserAgentParserDefaultProvider>(); + /// + /// public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( this IServiceCollection services) where TProvider : class, IHttpUserAgentParserProvider { diff --git a/src/HttpUserAgentParser/HttpUserAgentFastRules.cs b/src/HttpUserAgentParser/HttpUserAgentFastRules.cs new file mode 100644 index 0000000..c68f7f3 --- /dev/null +++ b/src/HttpUserAgentParser/HttpUserAgentFastRules.cs @@ -0,0 +1,296 @@ +// Copyright © https://myCSharp.de - all rights reserved + +using System.Collections.Concurrent; +using System.Text.RegularExpressions; + +namespace MyCSharp.HttpUserAgentParser; + +/// +/// Fast-path rules used by the parser to avoid heavy static initialization. +/// +internal static class HttpUserAgentFastRules +{ + /// + /// Regex defaults for platform mappings (fast-path cache only). + /// + private const RegexOptions DefaultPlatformsRegexFlags = RegexOptions.IgnoreCase | RegexOptions.Compiled; + + /// + /// Fast-path platform token rules for zero-allocation Contains checks. + /// Sorted by frequency for better performance (most common platforms first). + /// + internal static readonly (string Token, string Name, HttpUserAgentPlatformType PlatformType)[] s_platformRules = + [ + // Most common: Windows (specific versions before generic) + ("windows nt 10.0", "Windows 10", HttpUserAgentPlatformType.Windows), + ("windows nt 6.1", "Windows 7", HttpUserAgentPlatformType.Windows), + ("windows nt 6.3", "Windows 8.1", HttpUserAgentPlatformType.Windows), + ("windows nt 6.2", "Windows 8", HttpUserAgentPlatformType.Windows), + ("windows nt 6.0", "Windows Vista", HttpUserAgentPlatformType.Windows), + // Android (very common on mobile) + ("android", "Android", HttpUserAgentPlatformType.Android), + // iOS devices (very common) + ("iphone", "iOS", HttpUserAgentPlatformType.IOS), + ("ipad", "iOS", HttpUserAgentPlatformType.IOS), + ("ipod", "iOS", HttpUserAgentPlatformType.IOS), + // ChromeOS (must be before "os x" to avoid false match with "CrOS") + ("cros", "ChromeOS", HttpUserAgentPlatformType.ChromeOS), + // Mac OS (common) + ("os x", "Mac OS X", HttpUserAgentPlatformType.MacOS), + // Linux (common) + ("linux", "Linux", HttpUserAgentPlatformType.Linux), + // Other Windows versions + ("windows phone", "Windows Phone", HttpUserAgentPlatformType.Windows), + ("windows nt 5.2", "Windows 2003", HttpUserAgentPlatformType.Windows), + ("windows nt 5.1", "Windows XP", HttpUserAgentPlatformType.Windows), + ("windows nt 5.0", "Windows 2000", HttpUserAgentPlatformType.Windows), + ("windows nt 4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), + ("winnt4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), + ("winnt 4.0", "Windows NT", HttpUserAgentPlatformType.Windows), + ("winnt", "Windows NT", HttpUserAgentPlatformType.Windows), + ("windows 98", "Windows 98", HttpUserAgentPlatformType.Windows), + ("win98", "Windows 98", HttpUserAgentPlatformType.Windows), + ("windows 95", "Windows 95", HttpUserAgentPlatformType.Windows), + ("win95", "Windows 95", HttpUserAgentPlatformType.Windows), + ("windows", "Unknown Windows OS", HttpUserAgentPlatformType.Windows), + // Less common platforms + ("blackberry", "BlackBerry", HttpUserAgentPlatformType.BlackBerry), + ("ppc mac", "Power PC Mac", HttpUserAgentPlatformType.MacOS), + ("debian", "Debian", HttpUserAgentPlatformType.Linux), + ("freebsd", "FreeBSD", HttpUserAgentPlatformType.Linux), + ("ppc", "Macintosh", HttpUserAgentPlatformType.Linux), + ("gnu", "GNU/Linux", HttpUserAgentPlatformType.Linux), + ("unix", "Unknown Unix OS", HttpUserAgentPlatformType.Unix), + ("openbsd", "OpenBSD", HttpUserAgentPlatformType.Unix), + ("symbian", "Symbian OS", HttpUserAgentPlatformType.Symbian), + ("sunos", "Sun Solaris", HttpUserAgentPlatformType.Generic), + ("beos", "BeOS", HttpUserAgentPlatformType.Generic), + ("apachebench", "ApacheBench", HttpUserAgentPlatformType.Generic), + ("aix", "AIX", HttpUserAgentPlatformType.Generic), + ("irix", "Irix", HttpUserAgentPlatformType.Generic), + ("osf", "DEC OSF", HttpUserAgentPlatformType.Generic), + ("hp-ux", "HP-UX", HttpUserAgentPlatformType.Unix), + ("netbsd", "NetBSD", HttpUserAgentPlatformType.Generic), + ("bsdi", "BSDi", HttpUserAgentPlatformType.Generic), + ]; + + /// + /// Fast-path browser token rules. If these fail to extract a version, code will fall back to regex rules. + /// Sorted by specificity first, then frequency. + /// + internal static readonly (string Name, string DetectToken, string? VersionToken)[] s_browserRules = + [ + // Most specific browsers first (contain Chrome/Mozilla in their UA) + ("Opera", "OPR", null), + ("Opera", "Opera", "Version/"), + ("Opera", "Opera", null), + ("Edge", "Edg", null), + ("Edge", "Edge", null), + ("Edge", "EdgA", null), + ("Edge", "EdgiOS", null), + ("Brave", "Brave Chrome", null), + ("Vivaldi", "Vivaldi", null), + ("Flock", "Flock", null), + // Common browsers + ("Chrome", "Chrome", null), + ("Chrome", "CriOS", null), + ("Safari", "Version/", "Version/"), + ("Firefox", "Firefox", null), + ("Firefox", "FxiOS", null), + // Internet Explorer (legacy but still in use - MSIE before Trident to avoid false matches) + ("Internet Explorer", "MSIE", "MSIE "), + ("Internet Explorer", "Trident", "rv:"), + ("Internet Explorer", "Internet Explorer", null), + // Less common browsers + ("Maxthon", "Maxthon", null), + ("Netscape", "Netscape", null), + ("Konqueror", "Konqueror", null), + ("OmniWeb", "OmniWeb", null), + ("Shiira", "Shiira", null), + ("Chimera", "Chimera", null), + ("Camino", "Camino", null), + ("Firebird", "Firebird", null), + ("Phoenix", "Phoenix", null), + ("iCab", "icab", null), + ("Lynx", "Lynx", null), + ("Links", "Links", null), + ("HotJava", "hotjava", null), + ("Amaya", "amaya", null), + ("IBrowse", "IBrowse", null), + ("Apple iPod", "ipod touch", null), + ("Ubuntu Web Browser", "Ubuntu", null), + ]; + + /// + /// Mobile detection tokens (fast-path array to avoid dictionary enumeration overhead). + /// + internal static readonly (string Key, string Value)[] s_mobileRules = + [ + // Legacy + ("mobileexplorer", "Mobile Explorer"), + ("palmsource", "Palm"), + ("palmscape", "Palmscape"), + // Phones and Manufacturers + ("motorola", "Motorola"), + ("nokia", "Nokia"), + ("palm", "Palm"), + ("ipad", "Apple iPad"), + ("ipod", "Apple iPod"), + ("iphone", "Apple iPhone"), + ("sony", "Sony Ericsson"), + ("ericsson", "Sony Ericsson"), + ("blackberry", "BlackBerry"), + ("cocoon", "O2 Cocoon"), + ("blazer", "Treo"), + ("lg", "LG"), + ("amoi", "Amoi"), + ("xda", "XDA"), + ("mda", "MDA"), + ("vario", "Vario"), + ("htc", "HTC"), + ("samsung", "Samsung"), + ("sharp", "Sharp"), + ("sie-", "Siemens"), + ("alcatel", "Alcatel"), + ("benq", "BenQ"), + ("ipaq", "HP iPaq"), + ("mot-", "Motorola"), + ("playstation portable", "PlayStation Portable"), + ("playstation 3", "PlayStation 3"), + ("playstation vita", "PlayStation Vita"), + ("hiptop", "Danger Hiptop"), + ("nec-", "NEC"), + ("panasonic", "Panasonic"), + ("philips", "Philips"), + ("sagem", "Sagem"), + ("sanyo", "Sanyo"), + ("spv", "SPV"), + ("zte", "ZTE"), + ("sendo", "Sendo"), + ("nintendo dsi", "Nintendo DSi"), + ("nintendo ds", "Nintendo DS"), + ("nintendo 3ds", "Nintendo 3DS"), + ("wii", "Nintendo Wii"), + ("open web", "Open Web"), + ("openweb", "OpenWeb"), + // Operating Systems + ("android", "Android"), + ("symbian", "Symbian"), + ("SymbianOS", "SymbianOS"), + ("elaine", "Palm"), + ("series60", "Symbian S60"), + ("windows ce", "Windows CE"), + // Browsers + ("obigo", "Obigo"), + ("netfront", "Netfront Browser"), + ("openwave", "Openwave Browser"), + ("mobilexplorer", "Mobile Explorer"), + ("operamini", "Opera Mini"), + ("opera mini", "Opera Mini"), + ("opera mobi", "Opera Mobile"), + ("fennec", "Firefox Mobile"), + // Other + ("digital paths", "Digital Paths"), + ("avantgo", "AvantGo"), + ("xiino", "Xiino"), + ("novarra", "Novarra Transcoder"), + ("vodafone", "Vodafone"), + ("docomo", "NTT DoCoMo"), + ("o2", "O2"), + // Fallback + ("mobile", "Generic Mobile"), + ("wireless", "Generic Mobile"), + ("j2me", "Generic Mobile"), + ("midp", "Generic Mobile"), + ("cldc", "Generic Mobile"), + ("up.link", "Generic Mobile"), + ("up.browser", "Generic Mobile"), + ("smartphone", "Generic Mobile"), + ("cellphone", "Generic Mobile"), + ]; + + /// + /// Robot detection tokens. + /// + internal static readonly (string Key, string Value)[] s_robotRules = + [ + ( "googlebot", "Googlebot" ), + ( "meta-externalagent", "meta-externalagent" ), + ( "openai.com/searchbot", "OAI-SearchBot" ), + ( "CCBot", "CCBot" ), + ( "archive.org/details/archive.org_bot", "archive.org" ), + ( "opensiteexplorer.org/dotbot", "DotBot" ), + ( "awario.com/bots.html", "AwarioBot" ), + ( "Turnitin", "Turnitin" ), + ( "openai.com/gptbot", "GPTBot" ), + ( "perplexity.ai/perplexitybot", "PerplexityBot" ), + ( "developer.amazon.com/support/amazonbot", "Amazonbot" ), + ( "trendictionbot", "trendictionbot" ), + ( "Bytespider", "Bytespider" ), + ( "MojeekBot", "MojeekBot" ), + ( "SeekportBot", "SeekportBot" ), + ( "googleweblight", "Google Web Light" ), + ( "PetalBot", "PetalBot"), + ( "DuplexWeb-Google", "DuplexWeb-Google"), + ( "Storebot-Google", "Storebot-Google"), + ( "msnbot", "MSNBot"), + ( "baiduspider", "Baiduspider"), + ( "Google Favicon", "Google Favicon"), + ( "Jobboerse", "Jobboerse"), + ( "bingbot", "BingBot"), + ( "BingPreview", "Bing Preview"), + ( "slurp", "Slurp"), + ( "yahoo", "Yahoo"), + ( "ask jeeves", "Ask Jeeves"), + ( "fastcrawler", "FastCrawler"), + ( "infoseek", "InfoSeek Robot 1.0"), + ( "lycos", "Lycos"), + ( "YandexBot", "YandexBot"), + ( "YandexImages", "YandexImages"), + ( "mediapartners-google", "Mediapartners Google"), + ( "apis-google", "APIs Google"), + ( "CRAZYWEBCRAWLER", "Crazy Webcrawler"), + ( "AdsBot-Google-Mobile", "AdsBot Google Mobile"), + ( "adsbot-google", "AdsBot Google"), + ( "feedfetcher-google", "FeedFetcher-Google"), + ( "google-read-aloud", "Google-Read-Aloud"), + ( "curious george", "Curious George"), + ( "ia_archiver", "Alexa Crawler"), + ( "MJ12bot", "Majestic"), + ( "Uptimebot", "Uptimebot"), + ( "CheckMarkNetwork", "CheckMark"), + ( "facebookexternalhit", "Facebook"), + ( "adscanner", "AdScanner"), + ( "AhrefsBot", "Ahrefs"), + ( "BLEXBot", "BLEXBot"), + ( "DotBot", "OpenSite"), + ( "Mail.RU_Bot", "Mail.ru"), + ( "MegaIndex", "MegaIndex"), + ( "SemrushBot", "SEMRush"), + ( "SEOkicks", "SEOkicks"), + ( "seoscanners.net", "SEO Scanners"), + ( "Sistrix", "Sistrix" ), + ( "WhatsApp", "WhatsApp" ), + ( "CensysInspect", "CensysInspect" ), + ( "InternetMeasurement", "InternetMeasurement" ), + ( "Barkrowler", "Barkrowler" ), + ( "BrightEdge", "BrightEdge" ), + ( "ImagesiftBot", "ImagesiftBot" ), + ( "Cotoyogi", "Cotoyogi" ), + ( "Applebot", "Applebot" ), + ( "360Spider", "360Spider" ), + ( "GeedoProductSearch", "GeedoProductSearch" ) + ]; + + /// + /// Gets a cached regex for platform tokens without triggering heavy static initialization. + /// + internal static Regex GetPlatformRegexForToken(string token) + => s_platformRegexCache.GetOrAdd(token, + static t => new Regex(Regex.Escape(t), DefaultPlatformsRegexFlags, matchTimeout: TimeSpan.FromMilliseconds(1000))); + + /// + /// Cache for per-token platform regex instances. + /// + private static readonly ConcurrentDictionary s_platformRegexCache = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/src/HttpUserAgentParser/HttpUserAgentInformation.cs b/src/HttpUserAgentParser/HttpUserAgentInformation.cs index fc54e9b..cde02de 100644 --- a/src/HttpUserAgentParser/HttpUserAgentInformation.cs +++ b/src/HttpUserAgentParser/HttpUserAgentInformation.cs @@ -3,37 +3,45 @@ namespace MyCSharp.HttpUserAgentParser; /// -/// Analyzed user agent +/// Represents parsed information from an HTTP User-Agent string, including browser, platform, and device details. /// +/// +/// This is an immutable value type. Use or to create instances. +/// public readonly struct HttpUserAgentInformation { /// - /// Full User Agent string + /// Gets the original User-Agent string that was parsed. /// public string UserAgent { get; } /// - /// Type of user agent, see + /// Gets the type of the user agent (Browser, Robot, or Unknown). /// + /// public HttpUserAgentType Type { get; } /// - /// Platform of user agent, see + /// Gets the platform information, or if no platform was detected. /// + /// public HttpUserAgentPlatformInformation? Platform { get; } /// - /// Browser or Bot Name of user agent e.g. "Chrome", "Edge".. + /// Gets the browser or robot name (e.g., "Chrome", "Firefox", "Googlebot"), + /// or if not detected. /// public string? Name { get; } /// - /// Version of Browser or Bot Name of user agent e.g. "79.0", "83.0.125.4" + /// Gets the browser or robot version (e.g., "90.0.4430.212"), + /// or if not detected. /// public string? Version { get; } /// - /// Device Type of user agent, e.g. "Android", "Apple iPhone" + /// Gets the mobile device type (e.g., "Apple iPhone", "Android"), + /// or if not a mobile device. /// public string? MobileDeviceType { get; } @@ -51,8 +59,20 @@ private HttpUserAgentInformation(string userAgent, HttpUserAgentPlatformInformat } /// - /// Parses given User Agent + /// Parses the specified User-Agent string and returns parsed information. /// + /// The HTTP User-Agent header value to parse. + /// An instance containing the parsed data. + /// Thrown when is . + /// + /// + /// HttpUserAgentInformation info = HttpUserAgentInformation.Parse( + /// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/90.0.4430.212 Safari/537.36"); + /// + /// Console.WriteLine(info.Name); // "Chrome" + /// Console.WriteLine(info.Version); // "90.0.4430.212" + /// + /// public static HttpUserAgentInformation Parse(string userAgent) => HttpUserAgentParser.Parse(userAgent); /// diff --git a/src/HttpUserAgentParser/HttpUserAgentInformationExtensions.cs b/src/HttpUserAgentParser/HttpUserAgentInformationExtensions.cs index 409bafd..d8549ba 100644 --- a/src/HttpUserAgentParser/HttpUserAgentInformationExtensions.cs +++ b/src/HttpUserAgentParser/HttpUserAgentInformationExtensions.cs @@ -8,23 +8,56 @@ namespace MyCSharp.HttpUserAgentParser; public static class HttpUserAgentInformationExtensions { /// - /// Tests if is of + /// Determines whether the user agent is of the specified type. /// + /// The user agent information to check. + /// The type to compare against. + /// if the user agent matches the specified type; otherwise, . + /// + /// + /// HttpUserAgentInformation info = HttpUserAgentInformation.Parse("Mozilla/5.0 Chrome/90.0"); + /// bool isBrowser = info.IsType(HttpUserAgentType.Browser); + /// + /// public static bool IsType(this in HttpUserAgentInformation userAgent, HttpUserAgentType type) => userAgent.Type == type; /// - /// Tests if is of type + /// Determines whether the user agent is a robot/crawler/bot. /// + /// The user agent information to check. + /// if the user agent is a robot; otherwise, . + /// + /// + /// HttpUserAgentInformation info = HttpUserAgentInformation.Parse("Googlebot/2.1"); + /// bool isRobot = info.IsRobot(); // true + /// + /// public static bool IsRobot(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Robot); /// - /// Tests if is of type + /// Determines whether the user agent is a browser. /// + /// The user agent information to check. + /// if the user agent is a browser; otherwise, . + /// + /// + /// HttpUserAgentInformation info = HttpUserAgentInformation.Parse("Mozilla/5.0 Chrome/90.0"); + /// bool isBrowser = info.IsBrowser(); // true + /// + /// public static bool IsBrowser(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Browser); /// - /// returns true if agent is a mobile device + /// Determines whether the user agent represents a mobile device. /// - /// checks if is null + /// The user agent information to check. + /// if the user agent is from a mobile device; otherwise, . + /// This method checks if is not . + /// + /// + /// HttpUserAgentInformation info = HttpUserAgentInformation.Parse("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5)"); + /// bool isMobile = info.IsMobile(); // true + /// + /// public static bool IsMobile(this in HttpUserAgentInformation userAgent) => userAgent.MobileDeviceType is not null; } diff --git a/src/HttpUserAgentParser/HttpUserAgentParser.cs b/src/HttpUserAgentParser/HttpUserAgentParser.cs index 410637a..f9bc92f 100644 --- a/src/HttpUserAgentParser/HttpUserAgentParser.cs +++ b/src/HttpUserAgentParser/HttpUserAgentParser.cs @@ -11,13 +11,29 @@ namespace MyCSharp.HttpUserAgentParser; #pragma warning disable MA0049 // Type name should not match containing namespace /// -/// Parser logic for user agents +/// Provides methods to parse HTTP User-Agent strings and extract browser, platform, device, and robot information. /// +/// +/// This parser is optimized for performance using span-based operations and vectorized string matching. +/// For repeated parsing of the same user agent strings, consider using . +/// public static class HttpUserAgentParser { /// - /// Parses given user agent + /// Parses the specified User-Agent string and returns detailed information about the browser, platform, and device. /// + /// The HTTP User-Agent header value to parse. + /// An instance containing the parsed information. + /// Thrown when is . + /// + /// + /// string userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/90.0.4430.212 Safari/537.36"; + /// HttpUserAgentInformation info = HttpUserAgentParser.Parse(userAgentString); + /// + /// Console.WriteLine(info.Name); // "Chrome" + /// Console.WriteLine(info.Version); // "90.0.4430.212" + /// + /// public static HttpUserAgentInformation Parse(string userAgent) { // prepare @@ -41,22 +57,46 @@ public static HttpUserAgentInformation Parse(string userAgent) } /// - /// pre-cleanup of user agent + /// Removes leading and trailing whitespace from the User-Agent string. /// + /// The User-Agent string to clean up. + /// A trimmed copy of the User-Agent string. + /// + /// + /// string cleaned = HttpUserAgentParser.Cleanup(" Mozilla/5.0 "); + /// // Result: "Mozilla/5.0" + /// + /// public static string Cleanup(string userAgent) => userAgent.Trim(); /// - /// returns the platform or null + /// Extracts the platform information from the User-Agent string. /// + /// The User-Agent string to analyze. + /// + /// An instance if a platform is detected; otherwise, . + /// + /// + /// + /// HttpUserAgentPlatformInformation? platform = HttpUserAgentParser.GetPlatform( + /// "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + /// + /// if (platform != null) + /// { + /// Console.WriteLine(platform.Value.Name); // "Windows 10" + /// Console.WriteLine(platform.Value.PlatformType); // Windows + /// } + /// + /// public static HttpUserAgentPlatformInformation? GetPlatform(string userAgent) { ReadOnlySpan ua = userAgent.AsSpan(); - foreach ((string Token, string Name, HttpUserAgentPlatformType PlatformType) platform in HttpUserAgentStatics.s_platformRules) + foreach ((string Token, string Name, HttpUserAgentPlatformType PlatformType) platform in HttpUserAgentFastRules.s_platformRules) { if (ContainsIgnoreCase(ua, platform.Token)) { return new HttpUserAgentPlatformInformation( - HttpUserAgentStatics.GetPlatformRegexForToken(platform.Token), + HttpUserAgentFastRules.GetPlatformRegexForToken(platform.Token), platform.Name, platform.PlatformType); } } @@ -65,8 +105,18 @@ public static HttpUserAgentInformation Parse(string userAgent) } /// - /// returns true if platform was found + /// Attempts to extract the platform information from the User-Agent string. /// + /// The User-Agent string to analyze. + /// When this method returns , contains the platform information. + /// if a platform was detected; otherwise, . + /// + /// + /// bool found = HttpUserAgentParser.TryGetPlatform( + /// "Mozilla/5.0 (Windows NT 10.0)", + /// out HttpUserAgentPlatformInformation? platform); + /// + /// public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out HttpUserAgentPlatformInformation? platform) { platform = GetPlatform(userAgent); @@ -74,54 +124,71 @@ public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out Http } /// - /// returns the browser or null + /// Extracts the browser name and version from the User-Agent string. /// + /// The User-Agent string to analyze. + /// + /// A tuple containing the browser name and version if detected; otherwise, . + /// + /// + /// Uses a fast path with token-based matching to avoid regex where possible. + /// + /// + /// + /// (string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser( + /// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/90.0.4430.212 Safari/537.36"); + /// + /// if (browser != null) + /// { + /// Console.WriteLine(browser.Value.Name); // "Chrome" + /// Console.WriteLine(browser.Value.Version); // "90.0.4430.212" + /// } + /// + /// public static (string Name, string? Version)? GetBrowser(string userAgent) { ReadOnlySpan ua = userAgent.AsSpan(); - foreach ((string Name, string DetectToken, string? VersionToken) browserRule in HttpUserAgentStatics.s_browserRules) + foreach ((string Name, string DetectToken, string? VersionToken) browserRule in HttpUserAgentFastRules.s_browserRules) { if (!TryIndexOf(ua, browserRule.DetectToken, out int detectIndex)) { continue; } - // Version token may differ (e.g., Safari uses "Version/") - + int afterDetectIndex = detectIndex + browserRule.DetectToken.Length; int versionSearchStart; + // For rules without a specific version token, ensure pattern Token/ if (string.IsNullOrEmpty(browserRule.VersionToken)) { - int afterDetect = detectIndex + browserRule.DetectToken.Length; - if (afterDetect >= ua.Length || ua[afterDetect] != '/') + if (afterDetectIndex >= ua.Length || ua[afterDetectIndex] != '/') { // Likely a misspelling or partial token (e.g., Edgg, Oprea, Chromee) continue; } + versionSearchStart = afterDetectIndex; } - if (!string.IsNullOrEmpty(browserRule.VersionToken)) + else { - if (TryIndexOf(ua, browserRule.VersionToken!, out int vtIndex)) + // Version token may differ (e.g., Safari uses "Version/") + ReadOnlySpan afterDetect = afterDetectIndex < ua.Length ? ua.Slice(afterDetectIndex) : []; + + if (!afterDetect.IsEmpty && TryIndexOf(afterDetect, browserRule.VersionToken, out int vtIndex)) { - versionSearchStart = vtIndex + browserRule.VersionToken!.Length; + versionSearchStart = afterDetectIndex + vtIndex + browserRule.VersionToken.Length; } else { // If specific version token wasn't found, fall back to detect token area - versionSearchStart = detectIndex + browserRule.DetectToken.Length; + versionSearchStart = afterDetectIndex; } } - else - { - versionSearchStart = detectIndex + browserRule.DetectToken.Length; - } ReadOnlySpan search = ua.Slice(versionSearchStart); if (TryExtractVersion(search, out Range range)) { - string? version = search[range].ToString(); - return (browserRule.Name, version); + return (browserRule.Name, search[range].ToString()); } // If we didn't find a version for this rule, try next rule @@ -131,8 +198,18 @@ public static (string Name, string? Version)? GetBrowser(string userAgent) } /// - /// returns true if browser was found + /// Attempts to extract the browser name and version from the User-Agent string. /// + /// The User-Agent string to analyze. + /// When this method returns , contains the browser name and version. + /// if a browser was detected; otherwise, . + /// + /// + /// bool found = HttpUserAgentParser.TryGetBrowser( + /// "Mozilla/5.0 Chrome/90.0.4430.212", + /// out (string Name, string? Version)? browser); + /// + /// public static bool TryGetBrowser(string userAgent, [NotNullWhen(true)] out (string Name, string? Version)? browser) { browser = GetBrowser(userAgent); @@ -140,12 +217,20 @@ public static bool TryGetBrowser(string userAgent, [NotNullWhen(true)] out (stri } /// - /// returns the robot or null + /// Extracts the robot/bot name from the User-Agent string if it matches a known bot signature. /// + /// The User-Agent string to analyze. + /// The robot name if detected; otherwise, . + /// + /// + /// string? robot = HttpUserAgentParser.GetRobot("Googlebot/2.1 (+http://www.google.com/bot.html)"); + /// // Result: "Googlebot" + /// + /// public static string? GetRobot(string userAgent) { ReadOnlySpan ua = userAgent.AsSpan(); - foreach ((string key, string value) in HttpUserAgentStatics.Robots) + foreach ((string key, string value) in HttpUserAgentFastRules.s_robotRules) { if (ContainsIgnoreCase(ua, key)) { @@ -157,8 +242,16 @@ public static bool TryGetBrowser(string userAgent, [NotNullWhen(true)] out (stri } /// - /// returns true if robot was found + /// Attempts to extract the robot/bot name from the User-Agent string. /// + /// The User-Agent string to analyze. + /// When this method returns , contains the robot name. + /// if a robot was detected; otherwise, . + /// + /// + /// bool isBot = HttpUserAgentParser.TryGetRobot("Googlebot/2.1", out string? robotName); + /// + /// public static bool TryGetRobot(string userAgent, [NotNullWhen(true)] out string? robotName) { robotName = GetRobot(userAgent); @@ -166,12 +259,21 @@ public static bool TryGetRobot(string userAgent, [NotNullWhen(true)] out string? } /// - /// returns the device or null + /// Extracts the mobile device type from the User-Agent string. /// + /// The User-Agent string to analyze. + /// The device type (e.g., "Apple iPhone", "Android") if detected; otherwise, . + /// + /// + /// string? device = HttpUserAgentParser.GetMobileDevice( + /// "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5) Mobile"); + /// // Result: "Apple iPhone" + /// + /// public static string? GetMobileDevice(string userAgent) { ReadOnlySpan ua = userAgent.AsSpan(); - foreach ((string key, string value) in HttpUserAgentStatics.Mobiles) + foreach ((string key, string value) in HttpUserAgentFastRules.s_mobileRules) { if (ContainsIgnoreCase(ua, key)) { @@ -183,18 +285,34 @@ public static bool TryGetRobot(string userAgent, [NotNullWhen(true)] out string? } /// - /// returns true if device was found + /// Attempts to extract the mobile device type from the User-Agent string. /// + /// The User-Agent string to analyze. + /// When this method returns , contains the device type. + /// if a mobile device was detected; otherwise, . + /// + /// + /// bool isMobile = HttpUserAgentParser.TryGetMobileDevice( + /// "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5)", + /// out string? device); + /// + /// public static bool TryGetMobileDevice(string userAgent, [NotNullWhen(true)] out string? device) { device = GetMobileDevice(userAgent); return device is not null; } + /// + /// Fast case-insensitive substring check. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ContainsIgnoreCase(ReadOnlySpan haystack, ReadOnlySpan needle) => TryIndexOf(haystack, needle, out _); + /// + /// Finds the first index of a token in a span using ordinal-ignore-case. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryIndexOf(ReadOnlySpan haystack, ReadOnlySpan needle, out int index) { @@ -204,7 +322,7 @@ private static bool TryIndexOf(ReadOnlySpan haystack, ReadOnlySpan n /// /// Extracts a dotted numeric version. - /// Accepts digits and dots; skips common separators ('/', ' ', ':', '=') until first digit. + /// Accepts digits and dots; skips common separators until first digit. /// Returns false if no version-like token is found. /// private static bool TryExtractVersion(ReadOnlySpan haystack, out Range range) @@ -306,7 +424,7 @@ private static bool TryExtractVersion(ReadOnlySpan haystack, out Range ran for (; i < haystack.Length; ++i) { char c = haystack[i]; - if (char.IsBetween(c, '0', '9')) + if ((uint)(c - '0') <= 9) { start = i; break; @@ -323,7 +441,7 @@ private static bool TryExtractVersion(ReadOnlySpan haystack, out Range ran for (i = 0; i < haystack.Length; ++i) { char c = haystack[i]; - if (!(char.IsBetween(c, '0', '9') || c == '.')) + if (!((uint)(c - '0') <= 9 || c == '.')) { break; } diff --git a/src/HttpUserAgentParser/HttpUserAgentStatics.cs b/src/HttpUserAgentParser/HttpUserAgentStatics.cs index eead441..619c0aa 100644 --- a/src/HttpUserAgentParser/HttpUserAgentStatics.cs +++ b/src/HttpUserAgentParser/HttpUserAgentStatics.cs @@ -72,68 +72,14 @@ public static class HttpUserAgentStatics ]; /// - /// Fast-path platform token rules for zero-allocation Contains checks - /// Sorted by frequency for better performance (most common platforms first) + /// Precompiled platform regex map to attach to PlatformInformation without per-call allocations. /// - internal static readonly (string Token, string Name, HttpUserAgentPlatformType PlatformType)[] s_platformRules = - [ - // Most common: Windows (specific versions before generic) - ("windows nt 10.0", "Windows 10", HttpUserAgentPlatformType.Windows), - ("windows nt 6.1", "Windows 7", HttpUserAgentPlatformType.Windows), - ("windows nt 6.3", "Windows 8.1", HttpUserAgentPlatformType.Windows), - ("windows nt 6.2", "Windows 8", HttpUserAgentPlatformType.Windows), - ("windows nt 6.0", "Windows Vista", HttpUserAgentPlatformType.Windows), - // Android (very common on mobile) - ("android", "Android", HttpUserAgentPlatformType.Android), - // iOS devices (very common) - ("iphone", "iOS", HttpUserAgentPlatformType.IOS), - ("ipad", "iOS", HttpUserAgentPlatformType.IOS), - ("ipod", "iOS", HttpUserAgentPlatformType.IOS), - // ChromeOS (must be before "os x" to avoid false match with "CrOS") - ("cros", "ChromeOS", HttpUserAgentPlatformType.ChromeOS), - // Mac OS (common) - ("os x", "Mac OS X", HttpUserAgentPlatformType.MacOS), - // Linux (common) - ("linux", "Linux", HttpUserAgentPlatformType.Linux), - // Other Windows versions - ("windows phone", "Windows Phone", HttpUserAgentPlatformType.Windows), - ("windows nt 5.2", "Windows 2003", HttpUserAgentPlatformType.Windows), - ("windows nt 5.1", "Windows XP", HttpUserAgentPlatformType.Windows), - ("windows nt 5.0", "Windows 2000", HttpUserAgentPlatformType.Windows), - ("windows nt 4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), - ("winnt4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), - ("winnt 4.0", "Windows NT", HttpUserAgentPlatformType.Windows), - ("winnt", "Windows NT", HttpUserAgentPlatformType.Windows), - ("windows 98", "Windows 98", HttpUserAgentPlatformType.Windows), - ("win98", "Windows 98", HttpUserAgentPlatformType.Windows), - ("windows 95", "Windows 95", HttpUserAgentPlatformType.Windows), - ("win95", "Windows 95", HttpUserAgentPlatformType.Windows), - ("windows", "Unknown Windows OS", HttpUserAgentPlatformType.Windows), - // Less common platforms - ("blackberry", "BlackBerry", HttpUserAgentPlatformType.BlackBerry), - ("ppc mac", "Power PC Mac", HttpUserAgentPlatformType.MacOS), - ("debian", "Debian", HttpUserAgentPlatformType.Linux), - ("freebsd", "FreeBSD", HttpUserAgentPlatformType.Linux), - ("ppc", "Macintosh", HttpUserAgentPlatformType.Linux), - ("gnu", "GNU/Linux", HttpUserAgentPlatformType.Linux), - ("unix", "Unknown Unix OS", HttpUserAgentPlatformType.Unix), - ("openbsd", "OpenBSD", HttpUserAgentPlatformType.Unix), - ("symbian", "Symbian OS", HttpUserAgentPlatformType.Symbian), - ("sunos", "Sun Solaris", HttpUserAgentPlatformType.Generic), - ("beos", "BeOS", HttpUserAgentPlatformType.Generic), - ("apachebench", "ApacheBench", HttpUserAgentPlatformType.Generic), - ("aix", "AIX", HttpUserAgentPlatformType.Generic), - ("irix", "Irix", HttpUserAgentPlatformType.Generic), - ("osf", "DEC OSF", HttpUserAgentPlatformType.Generic), - ("hp-ux", "HP-UX", HttpUserAgentPlatformType.Windows), - ("netbsd", "NetBSD", HttpUserAgentPlatformType.Generic), - ("bsdi", "BSDi", HttpUserAgentPlatformType.Generic), - ]; - - // Precompiled platform regex map to attach to PlatformInformation without per-call allocations - private static readonly FrozenDictionary s_platformRegexMap = s_platformRules + private static readonly FrozenDictionary s_platformRegexMap = HttpUserAgentFastRules.s_platformRules .ToFrozenDictionary(p => p.Token, p => CreateDefaultPlatformRegex(p.Token), StringComparer.OrdinalIgnoreCase); + /// + /// Gets a precompiled platform regex for a given token. + /// internal static Regex GetPlatformRegexForToken(string token) => s_platformRegexMap[token]; /// @@ -188,215 +134,16 @@ private static Regex CreateDefaultBrowserRegex(string key) { CreateDefaultBrowserRegex("Ubuntu"), "Ubuntu Web Browser" }, }.ToFrozenDictionary(); - /// - /// Fast-path browser token rules. If these fail to extract a version, code will fall back to regex rules. - /// Sorted by specificity first, then frequency - more specific tokens must come before generic ones - /// (e.g., Edge/Opera before Chrome, since Edge/Opera UAs contain "Chrome") - /// - internal static readonly (string Name, string DetectToken, string? VersionToken)[] s_browserRules = - [ - // Most specific browsers first (contain Chrome/Mozilla in their UA) - ("Opera", "OPR", null), - ("Opera", "Opera", "Version/"), - ("Opera", "Opera", null), - ("Edge", "Edg", null), - ("Edge", "Edge", null), - ("Edge", "EdgA", null), - ("Edge", "EdgiOS", null), - ("Brave", "Brave Chrome", null), - ("Vivaldi", "Vivaldi", null), - ("Flock", "Flock", null), - // Common browsers - ("Chrome", "Chrome", null), - ("Chrome", "CriOS", null), - ("Safari", "Version/", "Version/"), - ("Firefox", "Firefox", null), - ("Firefox", "FxiOS", null), - // Internet Explorer (legacy but still in use - MSIE before Trident to avoid false matches) - ("Internet Explorer", "MSIE", "MSIE "), - ("Internet Explorer", "Trident", "rv:"), - ("Internet Explorer", "Internet Explorer", null), - // Less common browsers - ("Maxthon", "Maxthon", null), - ("Netscape", "Netscape", null), - ("Konqueror", "Konqueror", null), - ("OmniWeb", "OmniWeb", null), - ("Shiira", "Shiira", null), - ("Chimera", "Chimera", null), - ("Camino", "Camino", null), - ("Firebird", "Firebird", null), - ("Phoenix", "Phoenix", null), - ("iCab", "icab", null), - ("Lynx", "Lynx", null), - ("Links", "Links", null), - ("HotJava", "hotjava", null), - ("Amaya", "amaya", null), - ("IBrowse", "IBrowse", null), - ("Apple iPod", "ipod touch", null), - ("Ubuntu Web Browser", "Ubuntu", null), - ]; - /// /// Mobiles /// - public static readonly FrozenDictionary Mobiles = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - // Legacy - { "mobileexplorer", "Mobile Explorer" }, - { "palmsource", "Palm" }, - { "palmscape", "Palmscape" }, - // Phones and Manufacturers - { "motorola", "Motorola" }, - { "nokia", "Nokia" }, - { "palm", "Palm" }, - { "ipad", "Apple iPad" }, - { "ipod", "Apple iPod" }, - { "iphone", "Apple iPhone" }, - { "sony", "Sony Ericsson" }, - { "ericsson", "Sony Ericsson" }, - { "blackberry", "BlackBerry" }, - { "cocoon", "O2 Cocoon" }, - { "blazer", "Treo" }, - { "lg", "LG" }, - { "amoi", "Amoi" }, - { "xda", "XDA" }, - { "mda", "MDA" }, - { "vario", "Vario" }, - { "htc", "HTC" }, - { "samsung", "Samsung" }, - { "sharp", "Sharp" }, - { "sie-", "Siemens" }, - { "alcatel", "Alcatel" }, - { "benq", "BenQ" }, - { "ipaq", "HP iPaq" }, - { "mot-", "Motorola" }, - { "playstation portable", "PlayStation Portable" }, - { "playstation 3", "PlayStation 3" }, - { "playstation vita", "PlayStation Vita" }, - { "hiptop", "Danger Hiptop" }, - { "nec-", "NEC" }, - { "panasonic", "Panasonic" }, - { "philips", "Philips" }, - { "sagem", "Sagem" }, - { "sanyo", "Sanyo" }, - { "spv", "SPV" }, - { "zte", "ZTE" }, - { "sendo", "Sendo" }, - { "nintendo dsi", "Nintendo DSi" }, - { "nintendo ds", "Nintendo DS" }, - { "nintendo 3ds", "Nintendo 3DS" }, - { "wii", "Nintendo Wii" }, - { "open web", "Open Web" }, - { "openweb", "OpenWeb" }, - // Operating Systems - { "android", "Android" }, - { "symbian", "Symbian" }, - { "SymbianOS", "SymbianOS" }, - { "elaine", "Palm" }, - { "series60", "Symbian S60" }, - { "windows ce", "Windows CE" }, - // Browsers - { "obigo", "Obigo" }, - { "netfront", "Netfront Browser" }, - { "openwave", "Openwave Browser" }, - { "mobilexplorer", "Mobile Explorer" }, - { "operamini", "Opera Mini" }, - { "opera mini", "Opera Mini" }, - { "opera mobi", "Opera Mobile" }, - { "fennec", "Firefox Mobile" }, - // Other - { "digital paths", "Digital Paths" }, - { "avantgo", "AvantGo" }, - { "xiino", "Xiino" }, - { "novarra", "Novarra Transcoder" }, - { "vodafone", "Vodafone" }, - { "docomo", "NTT DoCoMo" }, - { "o2", "O2" }, - // Fallback - { "mobile", "Generic Mobile" }, - { "wireless", "Generic Mobile" }, - { "j2me", "Generic Mobile" }, - { "midp", "Generic Mobile" }, - { "cldc", "Generic Mobile" }, - { "up.link", "Generic Mobile" }, - { "up.browser", "Generic Mobile" }, - { "smartphone", "Generic Mobile" }, - { "cellphone", "Generic Mobile" }, - }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + public static readonly FrozenDictionary Mobiles = HttpUserAgentFastRules.s_mobileRules + .ToFrozenDictionary(mobile => mobile.Key, mobile => mobile.Value, StringComparer.OrdinalIgnoreCase); /// /// Robots /// - public static readonly (string Key, string Value)[] Robots = - [ - ( "googlebot", "Googlebot" ), - ( "meta-externalagent", "meta-externalagent" ), - ( "openai.com/searchbot", "OAI-SearchBot" ), - ( "CCBot", "CCBot" ), - ( "archive.org/details/archive.org_bot", "archive.org" ), - ( "opensiteexplorer.org/dotbot", "DotBot" ), - ( "awario.com/bots.html", "AwarioBot" ), - ( "Turnitin", "Turnitin" ), - ( "openai.com/gptbot", "GPTBot" ), - ( "perplexity.ai/perplexitybot", "PerplexityBot" ), - ( "developer.amazon.com/support/amazonbot", "Amazonbot" ), - ( "trendictionbot", "trendictionbot" ), - ( "openai.com/searchbot", "OAI-SearchBot" ), - ( "Bytespider", "Bytespider" ), - ( "MojeekBot", "MojeekBot" ), - ( "SeekportBot", "SeekportBot" ), - ( "googleweblight", "Google Web Light" ), - ( "PetalBot", "PetalBot"), - ( "DuplexWeb-Google", "DuplexWeb-Google"), - ( "Storebot-Google", "Storebot-Google"), - ( "msnbot", "MSNBot"), - ( "baiduspider", "Baiduspider"), - ( "Google Favicon", "Google Favicon"), - ( "Jobboerse", "Jobboerse"), - ( "bingbot", "BingBot"), - ( "BingPreview", "Bing Preview"), - ( "slurp", "Slurp"), - ( "yahoo", "Yahoo"), - ( "ask jeeves", "Ask Jeeves"), - ( "fastcrawler", "FastCrawler"), - ( "infoseek", "InfoSeek Robot 1.0"), - ( "lycos", "Lycos"), - ( "YandexBot", "YandexBot"), - ( "YandexImages", "YandexImages"), - ( "mediapartners-google", "Mediapartners Google"), - ( "apis-google", "APIs Google"), - ( "CRAZYWEBCRAWLER", "Crazy Webcrawler"), - ( "AdsBot-Google-Mobile", "AdsBot Google Mobile"), - ( "adsbot-google", "AdsBot Google"), - ( "feedfetcher-google", "FeedFetcher-Google"), - ( "google-read-aloud", "Google-Read-Aloud"), - ( "curious george", "Curious George"), - ( "ia_archiver", "Alexa Crawler"), - ( "MJ12bot", "Majestic"), - ( "Uptimebot", "Uptimebot"), - ( "CheckMarkNetwork", "CheckMark"), - ( "facebookexternalhit", "Facebook"), - ( "adscanner", "AdScanner"), - ( "AhrefsBot", "Ahrefs"), - ( "BLEXBot", "BLEXBot"), - ( "DotBot", "OpenSite"), - ( "Mail.RU_Bot", "Mail.ru"), - ( "MegaIndex", "MegaIndex"), - ( "SemrushBot", "SEMRush"), - ( "SEOkicks", "SEOkicks"), - ( "seoscanners.net", "SEO Scanners"), - ( "Sistrix", "Sistrix" ), - ( "WhatsApp", "WhatsApp" ), - ( "CensysInspect", "CensysInspect" ), - ( "InternetMeasurement", "InternetMeasurement" ), - ( "Barkrowler", "Barkrowler" ), - ( "BrightEdge", "BrightEdge" ), - ( "ImagesiftBot", "ImagesiftBot" ), - ( "Cotoyogi", "Cotoyogi" ), - ( "Applebot", "Applebot" ), - ( "360Spider", "360Spider" ), - ( "GeedoProductSearch", "GeedoProductSearch" ) - ]; + public static readonly (string Key, string Value)[] Robots = HttpUserAgentFastRules.s_robotRules; /// /// Tools diff --git a/src/HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs b/src/HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs index 381fd5b..e9acefd 100644 --- a/src/HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs +++ b/src/HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs @@ -5,28 +5,44 @@ namespace MyCSharp.HttpUserAgentParser.Providers; /// -/// In process cache provider for +/// Implementation of with in-memory caching using . /// +/// +/// Parsed results are cached indefinitely. The cache grows unbounded. +/// For production use with expiration support, consider using the +/// MyCSharp.HttpUserAgentParser.MemoryCache package instead. +/// public class HttpUserAgentParserCachedProvider : IHttpUserAgentParserProvider { - /// - /// internal cache - /// private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); - /// - /// Parses the user agent or uses the internal cached information - /// + /// + /// + /// + /// IHttpUserAgentParserProvider provider = new HttpUserAgentParserCachedProvider(); + /// HttpUserAgentInformation info = provider.Parse("Mozilla/5.0 Chrome/90.0.4430.212"); + /// // Subsequent calls with the same user agent return cached results. + /// + /// public HttpUserAgentInformation Parse(string userAgent) => _cache.GetOrAdd(userAgent, static ua => HttpUserAgentParser.Parse(ua)); /// - /// Total count of entries in cache + /// Gets the number of entries currently stored in the cache. /// public int CacheEntryCount => _cache.Count; /// - /// returns true if given user agent is in cache + /// Determines whether the specified user agent is already cached. /// + /// The user agent string to check. + /// if the user agent is in the cache; otherwise, . + /// + /// + /// HttpUserAgentParserCachedProvider provider = new HttpUserAgentParserCachedProvider(); + /// provider.Parse("Mozilla/5.0 Chrome/90.0"); + /// bool cached = provider.HasCacheEntry("Mozilla/5.0 Chrome/90.0"); // true + /// + /// public bool HasCacheEntry(string userAgent) => _cache.ContainsKey(userAgent); } diff --git a/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs b/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs index 7b3bd51..6b65610 100644 --- a/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs +++ b/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs @@ -3,13 +3,21 @@ namespace MyCSharp.HttpUserAgentParser.Providers; /// -/// Simple parse provider +/// Default implementation of without caching. /// +/// +/// Each call to performs a full parse operation. +/// For repeated parsing of the same user agents, consider using . +/// public class HttpUserAgentParserDefaultProvider : IHttpUserAgentParserProvider { - /// - /// returns the result of - /// + /// + /// + /// + /// IHttpUserAgentParserProvider provider = new HttpUserAgentParserDefaultProvider(); + /// HttpUserAgentInformation info = provider.Parse("Mozilla/5.0 Chrome/90.0.4430.212"); + /// + /// public HttpUserAgentInformation Parse(string userAgent) => HttpUserAgentParser.Parse(userAgent); } diff --git a/src/HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs b/src/HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs index dc127ba..1f24c0c 100644 --- a/src/HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs +++ b/src/HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs @@ -3,14 +3,20 @@ namespace MyCSharp.HttpUserAgentParser.Providers; /// -/// Provides the basic parsing of user agent strings. +/// Defines a contract for parsing HTTP User-Agent strings. /// +/// +/// Implementations may provide caching or other optimizations. +/// Use for simple parsing +/// or for in-memory caching. +/// public interface IHttpUserAgentParserProvider { /// - /// Parsed the -string. + /// Parses the specified User-Agent string and returns the parsed information. /// - /// The user agent to parse. - /// The parsed user agent information + /// The HTTP User-Agent header value to parse. + /// An instance containing the parsed data. + /// Thrown when is . HttpUserAgentInformation Parse(string userAgent); } diff --git a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs index 972f42a..d4a901c 100644 --- a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs +++ b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs @@ -288,6 +288,26 @@ public void GetBrowser_Trident_Without_RV_Falls_Back_To_Detect_Token() Assert.Equal("7.0", browser.Value.Version); } + [Fact] + public void GetBrowser_Opera_Uses_VersionToken_After_Detect() + { + // Detect token "Opera" with a separate "Version/" token after it + const string ua = "Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.16"; + + (string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser(ua); + Assert.NotNull(browser); + Assert.Equal("Opera", browser!.Value.Name); + Assert.Equal("12.16", browser.Value.Version); + } + + [Fact] + public void GetBrowser_VersionToken_With_Empty_Tail_Does_Not_Throw() + { + // Detect token present but no trailing characters, should fail gracefully + (string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser("Opera"); + Assert.Null(browser); + } + [Fact] public void GetBrowser_LongToken_NoDigits_Within_Window_Does_Not_Parse_Version() { diff --git a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentStaticsTests.cs b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentStaticsTests.cs new file mode 100644 index 0000000..a87ced7 --- /dev/null +++ b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentStaticsTests.cs @@ -0,0 +1,46 @@ +// Copyright © https://myCSharp.de - all rights reserved + +using System.Linq; +using System.Text.RegularExpressions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests; + +public class HttpUserAgentStaticsTests +{ + [Fact] + public void Platforms_Contain_Windows_Regex() + { + Assert.NotEmpty(HttpUserAgentStatics.Platforms); + + Regex regex = HttpUserAgentStatics.GetPlatformRegexForToken("windows nt 10.0"); + Assert.Matches(regex, "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + } + + [Fact] + public void Browsers_Contain_Chrome_Regex() + { + const string ua = "Mozilla/5.0 Chrome/90.0.4430.212 Safari/537.36"; + + KeyValuePair entry = HttpUserAgentStatics.Browsers + .First(candidate => string.Equals(candidate.Value, "Chrome", System.StringComparison.Ordinal) && candidate.Key.IsMatch(ua)); + + Match match = entry.Key.Match(ua); + Assert.True(match.Success); + Assert.Equal("90.0.4430.212", match.Groups[1].Value); + } + + [Fact] + public void Mobiles_Contains_Apple_IPhone() + { + bool found = HttpUserAgentStatics.Mobiles.TryGetValue("iphone", out string? name); + Assert.True(found); + Assert.Equal("Apple iPhone", name); + } + + [Fact] + public void Robots_Contains_Googlebot() + { + Assert.Contains(HttpUserAgentStatics.Robots, robot => robot.Key.Equals("googlebot", System.StringComparison.OrdinalIgnoreCase)); + } +}