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..5d07d44 100644
--- a/README.md
+++ b/README.md
@@ -141,7 +141,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);