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);