diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2ef0a8a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + // github copilot commit message instructions (preview) + "github.copilot.chat.commitMessageGeneration.instructions": [ + { "text": "Use conventional commit format: type(scope): description" }, + { "text": "Use imperative mood: 'Add feature' not 'Added feature'" }, + { "text": "Keep subject line under 50 characters" }, + { "text": "Use types: feat, fix, docs, style, refactor, perf, test, chore, ci" }, + { "text": "Include scope when relevant (e.g., api, ui, auth)" }, + { "text": "Reference issue numbers with # prefix" } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5ddca9a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "test", + "type": "shell", + "command": "dotnet test --nologo", + "args": [], + "problemMatcher": [ + "$msCompile" + ], + "group": "build" + } diff --git a/Directory.Packages.props b/Directory.Packages.props index f7ce539..eaa97d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,12 +3,16 @@ true + + + + - + diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..4b447ed --- /dev/null +++ b/Justfile @@ -0,0 +1,88 @@ +# Justfile .NET - Benjamin Abt 2025 - https://benjamin-abt.com +# https://github.com/BenjaminAbt/templates/blob/main/justfile/dotnet + +set shell := ["pwsh", "-c"] + +# ===== Configurable defaults ===== +CONFIG := "Debug" +TFM := "net10.0" +BENCH_PRJ := "perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj" + +# ===== Default / Help ===== +default: help + +help: + # Overview: + just --list + # Usage: + # just build + # just test + # just bench + +# ===== Basic .NET Workflows ===== +restore: + dotnet restore + +build *ARGS: + dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal {{ARGS}} + +rebuild *ARGS: + dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal --no-incremental {{ARGS}} + +clean: + dotnet clean --configuration "{{CONFIG}}" --nologo + +run *ARGS: + dotnet run --project --framework "{{TFM}}" --configuration "{{CONFIG}}" --no-launch-profile {{ARGS}} + +# ===== Quality / Tests ===== +format: + dotnet format --verbosity minimal + +format-check: + dotnet format --verify-no-changes --verbosity minimal + +test *ARGS: + dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal {{ARGS}} + +test-cov: + dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,lcov,opencover" /p:CoverletOutput="./TestResults/coverage/coverage" + + +test-filter QUERY: + dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal --filter "{{QUERY}}" + +# ===== Packaging / Release ===== +pack *ARGS: + dotnet pack --configuration "{{CONFIG}}" --nologo --verbosity minimal -o "./artifacts/packages" {{ARGS}} + +publish *ARGS: + dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}" {{ARGS}} + +publish-sc RID *ARGS: + dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --runtime "{{RID}}" --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}-{{RID}}" {{ARGS}} + +# ===== Benchmarks ===== +bench *ARGS: + dotnet run --configuration Release --project "{{BENCH_PRJ}}" --framework "{{TFM}}" {{ARGS}} + +# ===== Housekeeping ===== +clean-artifacts: + if (Test-Path "./artifacts") { Remove-Item "./artifacts" -Recurse -Force } + +clean-all: + just clean + just clean-artifacts + # Optionally: git clean -xdf + +# ===== Combined Flows ===== +fmt-build: + just format + just build + +ci: + just clean + just restore + just format-check + just build + just test-cov diff --git a/LICENSE b/LICENSE index 11152f9..023aed7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 MyCSharp +Copyright (c) 2021-2025 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/MyCSharp.HttpUserAgentParser.sln b/MyCSharp.HttpUserAgentParser.sln index 958aeb0..7596310 100644 --- a/MyCSharp.HttpUserAgentParser.sln +++ b/MyCSharp.HttpUserAgentParser.sln @@ -27,6 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{5738CE0D-5E6E-47 Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props global.json = global.json + Justfile = Justfile LICENSE = LICENSE NuGet.config = NuGet.config README.md = README.md @@ -81,6 +82,16 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {3357BEC0-8216-409E-A539-F9A71DBACB81} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {75960783-8BF9-479C-9ECF-E9653B74C9A2} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {3C8CCD44-F47C-4624-8997-54C42F02E376} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {A0D213E9-6408-46D1-AFAF-5096C2F6E027} = {FAAD18A0-E1B8-448D-B611-AFBDA8A89808} + {165EE915-1A4F-4875-90CE-1A2AE1540AE7} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E8B0C994-0BF2-4692-9E22-E48B265B2804} EndGlobalSection diff --git a/README.md b/README.md index 6ce902b..c43e521 100644 --- a/README.md +++ b/README.md @@ -112,22 +112,33 @@ public void MyMethod(IHttpUserAgentParserAccessor parserAccessor) ## Benchmark -```sh -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 -AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET Core SDK=5.0.300-preview.21228.15 - [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT +```shell +BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.6216/22H2/2022Update) +AMD Ryzen 9 9950X, 1 CPU, 32 logical and 16 physical cores +.NET SDK 10.0.100-preview.7.25380.108 + [Host] : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI + ShortRun : .NET 10.0.0 (10.0.25.38108), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 + +| Method | Categories | Data | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|------------------- |----------- |------------- |----------------:|-----------------:|---------------:|----------:|--------:|---------:|---------:|---------:|-----------:|------------:| +| MyCSharp | Basic | Chrome Win10 | 936.44 ns | 131.253 ns | 7.194 ns | 1.00 | 0.01 | 0.0029 | - | - | 48 B | 1.00 | +| UAParser | Basic | Chrome Win10 | 9,512,347.40 ns | 3,961,045.109 ns | 217,118.249 ns | 10,158.42 | 211.89 | 656.2500 | 546.8750 | 109.3750 | 11523315 B | 240,069.06 | +| DeviceDetector.NET | Basic | Chrome Win10 | 5,428,530.73 ns | 5,276,988.556 ns | 289,249.550 ns | 5,797.23 | 270.29 | 296.8750 | 125.0000 | 31.2500 | 5002239 B | 104,213.31 | +| | | | | | | | | | | | | | +| MyCSharp | Basic | Google-Bot | 165.66 ns | 21.926 ns | 1.202 ns | 1.00 | 0.01 | - | - | - | - | NA | +| UAParser | Basic | Google-Bot | 9,737,403.12 ns | 2,336,698.462 ns | 128,082.328 ns | 58,781.92 | 764.74 | 671.8750 | 656.2500 | 109.3750 | 11877003 B | NA | +| DeviceDetector.NET | Basic | Google-Bot | 6,331,960.42 ns | 1,602,716.199 ns | 87,850.283 ns | 38,224.23 | 518.30 | 500.0000 | 62.5000 | - | 8817013 B | NA | +| | | | | | | | | | | | | | +| MyCSharp | Cached | Chrome Win10 | 26.75 ns | 3.749 ns | 0.205 ns | 1.00 | 0.01 | - | - | - | - | NA | +| UAParser | Cached | Chrome Win10 | 250,039.55 ns | 6,502.182 ns | 356.407 ns | 9,346.54 | 63.39 | 2.1973 | - | - | 37488 B | NA | +| | | | | | | | | | | | | | +| MyCSharp | Cached | Google-Bot | 19.66 ns | 4.312 ns | 0.236 ns | 1.00 | 0.01 | - | - | - | - | NA | +| UAParser | Cached | Google-Bot | 184,991.85 ns | 46,235.986 ns | 2,534.350 ns | 9,408.77 | 148.82 | 2.6855 | - | - | 45857 B | NA | ``` -| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------- |------------:|----------:|----------:|--------:|-------:|------:|----------:| -| 'UA Parser' | 3,238.59 us | 27.435 us | 25.663 us | 7.8125 | - | - | 168225 B | -| UserAgentService | 391.11 us | 5.126 us | 4.795 us | 35.1563 | 3.4180 | - | 589664 B | -| HttpUserAgentParser | 67.07 us | 0.740 us | 0.693 us | - | - | - | 848 B | - -More benchmark results can be found [in this comment](https://github.com/mycsharp/HttpUserAgentParser/issues/2#issuecomment-842188532). - ## Disclaimer This library is inspired by [UserAgentService by DannyBoyNg](https://github.com/DannyBoyNg/UserAgentService) and contains optimizations for our requirements on [myCSharp.de](https://mycsharp.de). @@ -141,7 +152,7 @@ by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.c MIT License -Copyright (c) 2021-2023 MyCSharp +Copyright (c) 2021-2025 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/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj b/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj index e190b11..eb6a92c 100644 --- a/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj +++ b/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj @@ -5,6 +5,12 @@ disable + + + $(MSBuildProjectName) + $(MSBuildProjectName) + + $(DefineConstants);OS_WIN diff --git a/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParserBenchmarks.cs b/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParserBenchmarks.cs index acde85f..8b0b706 100644 --- a/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParserBenchmarks.cs +++ b/perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParserBenchmarks.cs @@ -2,12 +2,13 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; +using MyCSharp.HttpUserAgentParser; #if OS_WIN using BenchmarkDotNet.Diagnostics.Windows.Configs; #endif -namespace MyCSharp.HttpUserAgentParser.Benchmarks; +namespace HttpUserAgentParser.Benchmarks; [MemoryDiagnoser] [SimpleJob(RuntimeMoniker.Net80)] @@ -43,7 +44,7 @@ public void Parse() for (int i = 0; i < testUserAgentMix.Length; ++i) { - results[i] = HttpUserAgentParser.Parse(testUserAgentMix[i]); + results[i] = MyCSharp.HttpUserAgentParser.HttpUserAgentParser.Parse(testUserAgentMix[i]); } } } diff --git a/perf/HttpUserAgentParser.Benchmarks/LibraryComparison/LibraryComparisonBenchmarks.cs b/perf/HttpUserAgentParser.Benchmarks/LibraryComparison/LibraryComparisonBenchmarks.cs index 94a69b6..4d7bcb0 100644 --- a/perf/HttpUserAgentParser.Benchmarks/LibraryComparison/LibraryComparisonBenchmarks.cs +++ b/perf/HttpUserAgentParser.Benchmarks/LibraryComparison/LibraryComparisonBenchmarks.cs @@ -5,9 +5,10 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using DeviceDetectorNET; +using MyCSharp.HttpUserAgentParser; using MyCSharp.HttpUserAgentParser.Providers; -namespace MyCSharp.HttpUserAgentParser.Benchmarks.LibraryComparison; +namespace HttpUserAgentParser.Benchmarks.LibraryComparison; [ShortRunJob] [MemoryDiagnoser] @@ -33,7 +34,7 @@ public IEnumerable GetTestUserAgents() [BenchmarkCategory("Basic")] public HttpUserAgentInformation MyCSharpBasic() { - HttpUserAgentInformation info = HttpUserAgentParser.Parse(Data.UserAgent); + HttpUserAgentInformation info = MyCSharp.HttpUserAgentParser.HttpUserAgentParser.Parse(Data.UserAgent); return info; } diff --git a/src/HttpUserAgentParser.AspNetCore/LICENSE.txt b/src/HttpUserAgentParser.AspNetCore/LICENSE.txt index 11152f9..023aed7 100644 --- a/src/HttpUserAgentParser.AspNetCore/LICENSE.txt +++ b/src/HttpUserAgentParser.AspNetCore/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 MyCSharp +Copyright (c) 2021-2025 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.MemoryCache/LICENSE.txt b/src/HttpUserAgentParser.MemoryCache/LICENSE.txt index 11152f9..023aed7 100644 --- a/src/HttpUserAgentParser.MemoryCache/LICENSE.txt +++ b/src/HttpUserAgentParser.MemoryCache/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 MyCSharp +Copyright (c) 2021-2025 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/HttpUserAgentParser.cs b/src/HttpUserAgentParser/HttpUserAgentParser.cs index e32d5d8..5e788fd 100644 --- a/src/HttpUserAgentParser/HttpUserAgentParser.cs +++ b/src/HttpUserAgentParser/HttpUserAgentParser.cs @@ -1,7 +1,7 @@ // Copyright © https://myCSharp.de - all rights reserved using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; namespace MyCSharp.HttpUserAgentParser; @@ -48,11 +48,15 @@ public static HttpUserAgentInformation Parse(string userAgent) /// public static HttpUserAgentPlatformInformation? GetPlatform(string userAgent) { - foreach (HttpUserAgentPlatformInformation item in HttpUserAgentStatics.Platforms) + // Fast, allocation-free token scan (keeps public statics untouched) + ReadOnlySpan ua = userAgent.AsSpan(); + foreach ((string Token, string Name, HttpUserAgentPlatformType PlatformType) platform in HttpUserAgentStatics.s_platformRules) { - if (item.Regex.IsMatch(userAgent)) + if (ContainsIgnoreCase(ua, platform.Token)) { - return item; + return new HttpUserAgentPlatformInformation( + HttpUserAgentStatics.GetPlatformRegexForToken(platform.Token), + platform.Name, platform.PlatformType); } } @@ -73,13 +77,41 @@ public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out Http /// public static (string Name, string? Version)? GetBrowser(string userAgent) { - foreach ((Regex key, string? value) in HttpUserAgentStatics.Browsers) + ReadOnlySpan ua = userAgent.AsSpan(); + foreach ((string Name, string DetectToken, string? VersionToken) browserRule in HttpUserAgentStatics.s_browserRules) { - Match match = key.Match(userAgent); - if (match.Success) + if (!TryIndexOf(ua, browserRule.DetectToken, out int detectIndex)) { - return (value, match.Groups[1].Value); + continue; } + + // Version token may differ (e.g., Safari uses "Version/") + int versionSearchStart = detectIndex; + if (!string.IsNullOrEmpty(browserRule.VersionToken)) + { + if (TryIndexOf(ua, browserRule.VersionToken!, out int vtIndex)) + { + versionSearchStart = vtIndex + browserRule.VersionToken!.Length; + } + else + { + // If specific version token wasn't found, fall back to detect token area + versionSearchStart = detectIndex + browserRule.DetectToken.Length; + } + } + else + { + versionSearchStart = detectIndex + browserRule.DetectToken.Length; + } + + string? version = null; + ua = ua.Slice(versionSearchStart); + if (TryExtractVersion(ua, out Range range)) + { + version = ua[range].ToString(); + } + + return (browserRule.Name, version); } return null; @@ -143,4 +175,62 @@ public static bool TryGetMobileDevice(string userAgent, [NotNullWhen(true)] out device = GetMobileDevice(userAgent); return device is not null; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ContainsIgnoreCase(ReadOnlySpan haystack, ReadOnlySpan needle) + => TryIndexOf(haystack, needle, out _); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryIndexOf(ReadOnlySpan haystack, ReadOnlySpan needle, out int index) + { + index = haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + return index >= 0; + } + + /// + /// Extracts a dotted numeric version. + /// 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) + { + range = default; + + // Limit search window to avoid scanning entire UA string unnecessarily + const int Window = 128; + if (haystack.Length >= Window) + { + haystack = haystack.Slice(0, Window); + } + + int i = 0; + for (; i < haystack.Length; ++i) + { + char c = haystack[i]; + if (char.IsBetween(c, '0', '9')) + { + break; + } + } + + int s = i; + haystack = haystack.Slice(i + 1); + for (i = 0; i < haystack.Length; ++i) + { + char c = haystack[i]; + if (!(char.IsBetween(c, '0', '9') || c == '.')) + { + break; + } + } + i += s + 1; // shift back the previous domain + + if (i == s) + { + return false; + } + + range = new Range(s, i); + return true; + } } diff --git a/src/HttpUserAgentParser/HttpUserAgentStatics.cs b/src/HttpUserAgentParser/HttpUserAgentStatics.cs index 32d4580..3eca3f0 100644 --- a/src/HttpUserAgentParser/HttpUserAgentStatics.cs +++ b/src/HttpUserAgentParser/HttpUserAgentStatics.cs @@ -1,5 +1,6 @@ // Copyright © https://myCSharp.de - all rights reserved +using System.Collections.Frozen; using System.Text.RegularExpressions; namespace MyCSharp.HttpUserAgentParser; @@ -70,6 +71,62 @@ public static class HttpUserAgentStatics new(CreateDefaultPlatformRegex("symbian"), "Symbian OS", HttpUserAgentPlatformType.Symbian), ]; + /// + /// Fast-path platform token rules for zero-allocation Contains checks + /// + internal static readonly (string Token, string Name, HttpUserAgentPlatformType PlatformType)[] s_platformRules = + [ + ("windows nt 10.0", "Windows 10", HttpUserAgentPlatformType.Windows), + ("windows nt 6.3", "Windows 8.1", HttpUserAgentPlatformType.Windows), + ("windows nt 6.2", "Windows 8", HttpUserAgentPlatformType.Windows), + ("windows nt 6.1", "Windows 7", HttpUserAgentPlatformType.Windows), + ("windows nt 6.0", "Windows Vista", 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 phone", "Windows Phone", HttpUserAgentPlatformType.Windows), + ("windows", "Unknown Windows OS", HttpUserAgentPlatformType.Windows), + ("android", "Android", HttpUserAgentPlatformType.Android), + ("blackberry", "BlackBerry", HttpUserAgentPlatformType.BlackBerry), + ("iphone", "iOS", HttpUserAgentPlatformType.IOS), + ("ipad", "iOS", HttpUserAgentPlatformType.IOS), + ("ipod", "iOS", HttpUserAgentPlatformType.IOS), + ("cros", "ChromeOS", HttpUserAgentPlatformType.ChromeOS), + ("os x", "Mac OS X", HttpUserAgentPlatformType.MacOS), + ("ppc mac", "Power PC Mac", HttpUserAgentPlatformType.MacOS), + ("freebsd", "FreeBSD", HttpUserAgentPlatformType.Linux), + ("ppc", "Macintosh", HttpUserAgentPlatformType.Linux), + ("linux", "Linux", HttpUserAgentPlatformType.Linux), + ("debian", "Debian", HttpUserAgentPlatformType.Linux), + ("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), + ("openbsd", "OpenBSD", HttpUserAgentPlatformType.Unix), + ("gnu", "GNU/Linux", HttpUserAgentPlatformType.Linux), + ("unix", "Unknown Unix OS", HttpUserAgentPlatformType.Unix), + ("symbian", "Symbian OS", HttpUserAgentPlatformType.Symbian), + ]; + + // Precompiled platform regex map to attach to PlatformInformation without per-call allocations + private static readonly FrozenDictionary s_platformRegexMap = s_platformRules + .ToFrozenDictionary(p => p.Token, p => CreateDefaultPlatformRegex(p.Token), StringComparer.OrdinalIgnoreCase); + + internal static Regex GetPlatformRegexForToken(string token) => s_platformRegexMap[token]; + /// /// Regex defauls for browser mappings /// @@ -83,7 +140,7 @@ private static Regex CreateDefaultBrowserRegex(string key) /// /// Browsers /// - public static readonly Dictionary Browsers = new() + public static readonly FrozenDictionary Browsers = new Dictionary() { { CreateDefaultBrowserRegex("OPR"), "Opera" }, { CreateDefaultBrowserRegex("Flock"), "Flock" }, @@ -120,12 +177,54 @@ private static Regex CreateDefaultBrowserRegex(string key) { CreateDefaultBrowserRegex("Maxthon"), "Maxthon" }, { CreateDefaultBrowserRegex("ipod touch"), "Apple iPod" }, { 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. + /// + internal static readonly (string Name, string DetectToken, string? VersionToken)[] s_browserRules = + [ + ("Opera", "OPR", null), + ("Flock", "Flock", null), + ("Edge", "Edge", null), + ("Edge", "EdgA", null), + ("Edge", "Edg", null), + ("Vivaldi", "Vivaldi", null), + ("Brave", "Brave Chrome", null), + ("Chrome", "Chrome", null), + ("Chrome", "CriOS", null), + ("Opera", "Opera", "Version/"), + ("Opera", "Opera", null), + ("Internet Explorer", "MSIE", "MSIE "), + ("Internet Explorer", "Internet Explorer", null), + ("Internet Explorer", "Trident", "rv:"), + ("Shiira", "Shiira", null), + ("Firefox", "Firefox", null), + ("Firefox", "FxiOS", null), + ("Chimera", "Chimera", null), + ("Phoenix", "Phoenix", null), + ("Firebird", "Firebird", null), + ("Camino", "Camino", null), + ("Netscape", "Netscape", null), + ("OmniWeb", "OmniWeb", null), + ("Safari", "Version/", "Version/"), + ("Mozilla", "Mozilla", null), + ("Konqueror", "Konqueror", null), + ("iCab", "icab", null), + ("Lynx", "Lynx", null), + ("Links", "Links", null), + ("HotJava", "hotjava", null), + ("Amaya", "amaya", null), + ("IBrowse", "IBrowse", null), + ("Maxthon", "Maxthon", null), + ("Apple iPod", "ipod touch", null), + ("Ubuntu Web Browser", "Ubuntu", null), + ]; /// /// Mobiles /// - public static readonly Dictionary Mobiles = new(StringComparer.InvariantCultureIgnoreCase) + public static readonly FrozenDictionary Mobiles = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { // Legacy { "mobileexplorer", "Mobile Explorer" }, @@ -208,7 +307,7 @@ private static Regex CreateDefaultBrowserRegex(string key) { "up.browser", "Generic Mobile" }, { "smartphone", "Generic Mobile" }, { "cellphone", "Generic Mobile" }, - }; + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); /// /// Robots @@ -287,8 +386,9 @@ public static readonly (string Key, string Value)[] Robots = /// /// Tools /// - public static readonly Dictionary Tools = new(StringComparer.OrdinalIgnoreCase) + public static readonly FrozenDictionary Tools = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "curl", "curl" } - }; + } + .ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } diff --git a/src/HttpUserAgentParser/LICENSE.txt b/src/HttpUserAgentParser/LICENSE.txt index 11152f9..023aed7 100644 --- a/src/HttpUserAgentParser/LICENSE.txt +++ b/src/HttpUserAgentParser/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 MyCSharp +Copyright (c) 2021-2025 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/Providers/HttpUserAgentParserDefaultProvider.cs b/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs index f43e3c4..7b3bd51 100644 --- a/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs +++ b/src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs @@ -8,7 +8,7 @@ namespace MyCSharp.HttpUserAgentParser.Providers; public class HttpUserAgentParserDefaultProvider : IHttpUserAgentParserProvider { /// - /// returns the result of + /// returns the result of /// public HttpUserAgentInformation Parse(string userAgent) => HttpUserAgentParser.Parse(userAgent);