From 5cf42ba9c7d0aeb2970c0d0ee0d33a9dbacd7f05 Mon Sep 17 00:00:00 2001 From: Eugene Smelov Date: Tue, 5 May 2020 20:41:38 +0300 Subject: [PATCH 1/2] added netcore 3, net standart 2 support, improve performance --- UAParser.ConsoleApp/Program.cs | 55 +- .../UAParser.ConsoleApp.csproj | 2 +- UAParser.Tests.Benchmark/Program.cs | 10 + .../UAParser.Tests.Benchmark.csproj | 22 + UAParser.Tests.Benchmark/UAParserTests.cs | 54 ++ UAParser.Tests/ParserTests.cs | 2 +- UAParser.Tests/TestResourceTests.cs | 7 +- UAParser.sln | 10 +- UAParser/Abstraction/AbstractParser.cs | 129 ++++ UAParser/Abstraction/IParser.cs | 7 + UAParser/Abstraction/IUAParserOutput.cs | 31 + UAParser/Abstraction/Template.cs | 14 + UAParser/AssemblyInfo.cs | 2 +- UAParser/Device.cs | 64 ++ UAParser/Extensions/DictionaryExtension.cs | 14 + UAParser/Extensions/StringExtension.cs | 16 + UAParser/Implementation/ClientInfo.cs | 61 ++ UAParser/Implementation/DeviceParser.cs | 76 ++ UAParser/Implementation/DeviceTemplate.cs | 22 + UAParser/Implementation/OSParser.cs | 70 ++ UAParser/Implementation/OsTemplate.cs | 29 + UAParser/Implementation/UserAgentParser.cs | 40 + UAParser/Implementation/UserAgentTemplate.cs | 39 + UAParser/MinimalYamlParser.cs | 194 +++++ UAParser/OS.cs | 70 ++ UAParser/ParserOptions.cs | 47 ++ UAParser/Part.cs | 22 + UAParser/UAParser.cs | 687 +----------------- UAParser/UAParser.csproj | 72 +- UAParser/UserAgent.cs | 63 ++ 30 files changed, 1230 insertions(+), 701 deletions(-) create mode 100644 UAParser.Tests.Benchmark/Program.cs create mode 100644 UAParser.Tests.Benchmark/UAParser.Tests.Benchmark.csproj create mode 100644 UAParser.Tests.Benchmark/UAParserTests.cs create mode 100644 UAParser/Abstraction/AbstractParser.cs create mode 100644 UAParser/Abstraction/IParser.cs create mode 100644 UAParser/Abstraction/IUAParserOutput.cs create mode 100644 UAParser/Abstraction/Template.cs create mode 100644 UAParser/Device.cs create mode 100644 UAParser/Extensions/DictionaryExtension.cs create mode 100644 UAParser/Extensions/StringExtension.cs create mode 100644 UAParser/Implementation/ClientInfo.cs create mode 100644 UAParser/Implementation/DeviceParser.cs create mode 100644 UAParser/Implementation/DeviceTemplate.cs create mode 100644 UAParser/Implementation/OSParser.cs create mode 100644 UAParser/Implementation/OsTemplate.cs create mode 100644 UAParser/Implementation/UserAgentParser.cs create mode 100644 UAParser/Implementation/UserAgentTemplate.cs create mode 100644 UAParser/MinimalYamlParser.cs create mode 100644 UAParser/OS.cs create mode 100644 UAParser/ParserOptions.cs create mode 100644 UAParser/Part.cs create mode 100644 UAParser/UserAgent.cs diff --git a/UAParser.ConsoleApp/Program.cs b/UAParser.ConsoleApp/Program.cs index c84bef5..7fb3ede 100644 --- a/UAParser.ConsoleApp/Program.cs +++ b/UAParser.ConsoleApp/Program.cs @@ -1,34 +1,37 @@ namespace UAParser.ConsoleApp { - using System; - using System.Linq; + using System; + using System.Linq; - static class Program - { - static void Main(string[] args) + static class Program { - if (args.Any(arg => arg == "-?" || arg == "-h" || arg == "--help")) - { - Help(); - return; - } + static void Main(string[] args) + { + if (args.Any(arg => arg == "-?" || arg == "-h" || arg == "--help")) + { + Help(); + return; + } - var uaParser = Parser.GetDefault(); - string uaString; - while ((uaString = Console.In.ReadLine()) != null) - { - uaString = uaString.Trim(); - if (uaString.Length == 0) - continue; - var c = uaParser.Parse(uaString); - Console.WriteLine("Agent : {0}", c.UA); - Console.WriteLine("OS : {0}", c.OS); - Console.WriteLine("Device: {0}", c.Device); - } - } + Console.ReadKey(); - static void Help() - { + var uaParser = Parser.GetDefault(); + string uaString; + while ((uaString = Console.In.ReadLine()) != null) + { + uaString = uaString.Trim(); + if (uaString.Length == 0) + continue; + Console.ReadKey(); + var c = uaParser.Parse(uaString); + Console.WriteLine("Agent : {0}", c.UA); + Console.WriteLine("OS : {0}", c.OS); + Console.WriteLine("Device: {0}", c.Device); + } + } + + static void Help() + { Console.WriteLine(@"UAParser Copyright 2015 " + "S\u00f8ren Enem\u00e6rke" + @" https://github.com/tobie/ua-parser @@ -36,6 +39,6 @@ Copyright 2015 " + "S\u00f8ren Enem\u00e6rke" + @" This application accepts user agent strings (one per line) from standard input, parses them and then emits the identified agent, operating system and device for each string."); + } } - } } diff --git a/UAParser.ConsoleApp/UAParser.ConsoleApp.csproj b/UAParser.ConsoleApp/UAParser.ConsoleApp.csproj index ca9b314..0f4cd51 100644 --- a/UAParser.ConsoleApp/UAParser.ConsoleApp.csproj +++ b/UAParser.ConsoleApp/UAParser.ConsoleApp.csproj @@ -67,4 +67,4 @@ --> - + \ No newline at end of file diff --git a/UAParser.Tests.Benchmark/Program.cs b/UAParser.Tests.Benchmark/Program.cs new file mode 100644 index 0000000..bb3efcc --- /dev/null +++ b/UAParser.Tests.Benchmark/Program.cs @@ -0,0 +1,10 @@ +using BenchmarkDotNet.Running; + +namespace UAParser.Tests.Benchmark +{ + class Program + { + static void Main(string[] args) + => BenchmarkRunner.Run(); + } +} diff --git a/UAParser.Tests.Benchmark/UAParser.Tests.Benchmark.csproj b/UAParser.Tests.Benchmark/UAParser.Tests.Benchmark.csproj new file mode 100644 index 0000000..bda2422 --- /dev/null +++ b/UAParser.Tests.Benchmark/UAParser.Tests.Benchmark.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1 + true + ..\PublicKey.snk + + + + + + + + + + + + + + + diff --git a/UAParser.Tests.Benchmark/UAParserTests.cs b/UAParser.Tests.Benchmark/UAParserTests.cs new file mode 100644 index 0000000..75a3f8c --- /dev/null +++ b/UAParser.Tests.Benchmark/UAParserTests.cs @@ -0,0 +1,54 @@ +using BenchmarkDotNet.Attributes; +using System; +using System.IO; +using System.Text; + +namespace UAParser.Tests.Benchmark +{ + [MemoryDiagnoser] + public class UAParserTests + { + private static readonly string _yamlString = GetTestResources("UAParser.Tests.Benchmark.Regexes.regexes.yaml"); + private static readonly Parser _parser = Parser.GetDefault(); + + [Params( + "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 7.1.1; G8231 Build/41.2.A.0.219; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1")] + public string UserAgentString { get; set; } + + [Benchmark] + public ClientInfo UAParseTest() + { + return _parser.Parse(UserAgentString); + } + + [Benchmark] + public bool ReadYaml() + { + var yamlParser = new MinimalYamlParser(_yamlString); + + return yamlParser != null; + } + + [Benchmark] + public Parser CreateParser() + { + return Parser.GetDefault(); + } + + internal static string GetTestResources(string name) + { + using (var s = typeof(UAParserTests).Assembly.GetManifestResourceStream(name)) + { + if (s == null) + throw new InvalidOperationException("Could not locate an embedded test resource with name: " + name); + using (var sr = new StreamReader(s, Encoding.UTF8)) + { + return sr.ReadToEnd(); + } + } + } + } +} diff --git a/UAParser.Tests/ParserTests.cs b/UAParser.Tests/ParserTests.cs index 9248174..24d92b4 100644 --- a/UAParser.Tests/ParserTests.cs +++ b/UAParser.Tests/ParserTests.cs @@ -29,7 +29,7 @@ public void can_get_parser_from_input() public void can_utilize_regex_timeouts() { string yamlContent = this.GetTestResources("UAParser.Tests.Regexes.backtracking.yaml"); - Parser parser = Parser.FromYaml(yamlContent, new ParserOptions() + Parser parser = Parser.FromYaml(yamlContent, new ParserOptions { MatchTimeOut = TimeSpan.FromSeconds(1), }); diff --git a/UAParser.Tests/TestResourceTests.cs b/UAParser.Tests/TestResourceTests.cs index 3d5d8dc..c20bb8b 100644 --- a/UAParser.Tests/TestResourceTests.cs +++ b/UAParser.Tests/TestResourceTests.cs @@ -81,13 +81,12 @@ private static void RunTestCases(List testCases) where TTe sb.AppendLine($"test case {(i + 1)}: {ex.Message}"); } } + Assert.True(0 == sb.Length, "Failed tests: " + Environment.NewLine + sb); } - public List GetTestCases( - string resourceName, - string yamlNodeName, - Func, TTestCase> testCaseFunction) + public List GetTestCases(string resourceName, string yamlNodeName, + Func, TTestCase> testCaseFunction) { string yamlContent = this.GetTestResources(resourceName); YamlStream yaml = new YamlStream(); diff --git a/UAParser.sln b/UAParser.sln index 2c8b6bf..3e42e5b 100644 --- a/UAParser.sln +++ b/UAParser.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UAParser.ConsoleApp", "UAParser.ConsoleApp\UAParser.ConsoleApp.csproj", "{280CF383-6A6E-487B-9D25-281C98F3A7C8}" EndProject @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UAParser", "UAParser\UAPars EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UAParser.Tests", "UAParser.Tests\UAParser.Tests.csproj", "{754416BE-F962-4898-832A-739B0EF6C3CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UAParser.Tests.Benchmark", "UAParser.Tests.Benchmark\UAParser.Tests.Benchmark.csproj", "{ECFBC2D2-1E86-4121-B001-9AE522E7F9FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {754416BE-F962-4898-832A-739B0EF6C3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {754416BE-F962-4898-832A-739B0EF6C3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {754416BE-F962-4898-832A-739B0EF6C3CC}.Release|Any CPU.Build.0 = Release|Any CPU + {ECFBC2D2-1E86-4121-B001-9AE522E7F9FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECFBC2D2-1E86-4121-B001-9AE522E7F9FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECFBC2D2-1E86-4121-B001-9AE522E7F9FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECFBC2D2-1E86-4121-B001-9AE522E7F9FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/UAParser/Abstraction/AbstractParser.cs b/UAParser/Abstraction/AbstractParser.cs new file mode 100644 index 0000000..01accec --- /dev/null +++ b/UAParser/Abstraction/AbstractParser.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace UAParser.Abstraction +{ + internal abstract class AbstractParser : IParser + where TOut : Part + where TTemplate : Template + { + protected static readonly string[] _allReplacementTokens = new string[] + { "$1","$2","$3","$4","$5","$6","$7","$8","$91" }; + + private readonly ParserOptions _options; + + protected AbstractParser(IEnumerable> maps, ParserOptions options, TOut defaultValue) + { + _options = options; + DefaultValue = defaultValue; + Templates = maps.Select(InitializeTemplate).ToList(); + } + + protected List Templates { get; } + + protected TOut DefaultValue { get; } + + public virtual TOut Parse(string input) + { + foreach (var template in Templates) + { + var device = Matcher(input, template); + if (device != default) + return device; + } + + return DefaultValue; + } + + protected abstract TOut Matcher(string input, TTemplate template); + + protected virtual Match Match(string input, Regex regex) + { +#if REGEX_MATCHTIMEOUT + try + { + return regex.Match(input); + } + catch (RegexMatchTimeoutException) + { + // we'll simply swallow this exception and return the default (non-matched) + return default; + } +#else + return regex.Match(input); +#endif + } + + protected abstract TTemplate InitializeTemplate(IDictionary map); + + protected Regex Regex(IDictionary indexer, string key, string regexFlag = null) + { + var pattern = indexer.Find("regex"); + if (pattern == null) + throw new Exception($"{key} is missing regular expression specification."); + + // Some expressions in the regex.yaml file causes parsing errors + // in .NET such as the \_ token so need to alter them before + // proceeding. + + if (pattern.IndexOf(@"\_", StringComparison.Ordinal) >= 0) + pattern = pattern.Replace(@"\_", "_"); + + //Singleline: User agent strings do not contain newline characters. RegexOptions.Singleline improves performance. + //CultureInvariant: The interpretation of a user agent never depends on the current locale. + var options = RegexOptions.Singleline | RegexOptions.CultureInvariant; + + if ("i".Equals(regexFlag)) + { + options |= RegexOptions.IgnoreCase; + } + +#if REGEX_COMPILATION + if (_options.UseCompiledRegex) + { + options |= RegexOptions.Compiled; + } +#endif + +#if REGEX_MATCHTIMEOUT + + return new Regex(pattern, options, _options.MatchTimeOut); +#else + return new Regex(pattern, options); +#endif + } + + protected string Replace(Match m, IEnumerator num, string replacement) + { + return replacement != null + ? Select(_ => replacement, m, num) + : Select(x => x, m, num); + } + + protected string Replace(Match m, IEnumerator num, string replacement, string token) + { + return replacement != null && replacement.Contains(token) + ? Select(s => s != null ? replacement.ReplaceFirstOccurence(token, s) : replacement, m, num) + : Replace(m, num, replacement); + } + + protected string Select(Func selector, Match m, IEnumerator num) + { + if (!num.MoveNext()) throw new InvalidOperationException(); + var groups = m.Groups; Group group; + return selector( + num.Current <= groups.Count && (group = groups[num.Current]).Success + ? group.Value.Trim() + : null); + } + + protected IEnumerator Generate(T initial, Func next) + { + for (var state = initial; ; state = next(state)) + yield return state; + // ReSharper disable once FunctionNeverReturns + } + } +} diff --git a/UAParser/Abstraction/IParser.cs b/UAParser/Abstraction/IParser.cs new file mode 100644 index 0000000..9ef0b90 --- /dev/null +++ b/UAParser/Abstraction/IParser.cs @@ -0,0 +1,7 @@ +namespace UAParser.Abstraction +{ + internal interface IParser + { + T Parse(string input); + } +} diff --git a/UAParser/Abstraction/IUAParserOutput.cs b/UAParser/Abstraction/IUAParserOutput.cs new file mode 100644 index 0000000..ca432e1 --- /dev/null +++ b/UAParser/Abstraction/IUAParserOutput.cs @@ -0,0 +1,31 @@ +namespace UAParser.Abstraction +{ + /// + /// Representing the parse results. Structure of this class aligns with the + /// ua-parser-output WebIDL structure defined in this document: https://github.com/ua-parser/uap-core/blob/master/docs/specification.md + /// + public interface IUAParserOutput + { + /// + /// The user agent string, the input for the UAParser + /// + string String { get; } + + /// + /// The OS parsed from the user agent string + /// + // ReSharper disable once InconsistentNaming + OS OS { get; } + + /// + /// The Device parsed from the user agent string + /// + Device Device { get; } + + // ReSharper disable once InconsistentNaming + /// + /// The User Agent parsed from the user agent string + /// + UserAgent UA { get; } + } +} diff --git a/UAParser/Abstraction/Template.cs b/UAParser/Abstraction/Template.cs new file mode 100644 index 0000000..c9dbafa --- /dev/null +++ b/UAParser/Abstraction/Template.cs @@ -0,0 +1,14 @@ +using System.Text.RegularExpressions; + +namespace UAParser.Abstraction +{ + internal abstract class Template + { + protected Template(Regex regex) + { + Regex = regex; + } + + public Regex Regex { get; } + } +} diff --git a/UAParser/AssemblyInfo.cs b/UAParser/AssemblyInfo.cs index 30c128e..788e54e 100644 --- a/UAParser/AssemblyInfo.cs +++ b/UAParser/AssemblyInfo.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -8,3 +7,4 @@ [assembly: ComVisible(false)] [assembly: InternalsVisibleTo("UAParser.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a7c49ce2884f71f640072402131fc866e654c982958f605a1ea0162b512138a87eab09c3335835f6d56231732f609fb9c48b03af24c9cce40cdcc1ac08ff8821cf3413a319770520f9c019d3ed6a185d60b673271c1f2fb380951c37290c99fbd4cd3a3db70bd48d524cf91ce9323e6b3c02765a2790e23a0419c05751b00498")] +[assembly: InternalsVisibleTo("UAParser.Tests.Benchmark, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a7c49ce2884f71f640072402131fc866e654c982958f605a1ea0162b512138a87eab09c3335835f6d56231732f609fb9c48b03af24c9cce40cdcc1ac08ff8821cf3413a319770520f9c019d3ed6a185d60b673271c1f2fb380951c37290c99fbd4cd3a3db70bd48d524cf91ce9323e6b3c02765a2790e23a0419c05751b00498")] diff --git a/UAParser/Device.cs b/UAParser/Device.cs new file mode 100644 index 0000000..3110a1e --- /dev/null +++ b/UAParser/Device.cs @@ -0,0 +1,64 @@ +#region Apache License, Version 2.0 +// +// Copyright 2014 Atif Aziz +// Portions Copyright 2012 Søren Enemærke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +namespace UAParser +{ + using System; + + /// + /// Represents the physical device the user agent is using + /// + public sealed class Device : Part + { + /// + /// Constructs a Device instance + /// + public Device(string family, string brand, string model) + : base (family) + { + if (brand != null) + Brand = brand; + if (model != null) + Model = model; + } + + /// + /// Returns true if the device is likely to be a spider or a bot device + /// + public bool IsSpider => "Spider".Equals(Family, StringComparison.OrdinalIgnoreCase); + + /// + ///The brand of the device + /// + public string Brand { get; } + + /// + /// The model of the device, if available + /// + public string Model { get; } + + /// + /// A readable description of the device + /// + public override string ToString() + { + return Family; + } + } +} diff --git a/UAParser/Extensions/DictionaryExtension.cs b/UAParser/Extensions/DictionaryExtension.cs new file mode 100644 index 0000000..75c8567 --- /dev/null +++ b/UAParser/Extensions/DictionaryExtension.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace UAParser +{ + internal static class DictionaryExtension + { + public static TValue Find(this IDictionary dictionary, TKey key) + { + if (dictionary == null) throw new ArgumentNullException(nameof(dictionary)); + return dictionary.TryGetValue(key, out var result) ? result : default; + } + } +} diff --git a/UAParser/Extensions/StringExtension.cs b/UAParser/Extensions/StringExtension.cs new file mode 100644 index 0000000..402829d --- /dev/null +++ b/UAParser/Extensions/StringExtension.cs @@ -0,0 +1,16 @@ +using System; + +namespace UAParser +{ + internal static class StringExtension + { + public static string ReplaceFirstOccurence(this string input, string search, string replacement) + { + if (input == null) throw new ArgumentNullException(nameof(input)); + var index = input.IndexOf(search, StringComparison.Ordinal); + return index >= 0 + ? input.Substring(0, index) + replacement + input.Substring(index + search.Length) + : input; + } + } +} diff --git a/UAParser/Implementation/ClientInfo.cs b/UAParser/Implementation/ClientInfo.cs new file mode 100644 index 0000000..4c59ff1 --- /dev/null +++ b/UAParser/Implementation/ClientInfo.cs @@ -0,0 +1,61 @@ +using System; +using UAParser.Abstraction; + +namespace UAParser +{ + /// + /// Represents the user agent client information resulting from parsing + /// a user agent string + /// + public class ClientInfo : IUAParserOutput + { + /// + /// The user agent string, the input for the UAParser + /// + public string String { get; } + + // ReSharper disable once InconsistentNaming + /// + /// The OS parsed from the user agent string + /// + // ReSharper disable once InconsistentNaming + public OS OS { get; } + + /// + /// The Device parsed from the user agent string + /// + public Device Device { get; } + + /// + /// The User Agent parsed from the user agent string + /// + [Obsolete("Mirrors the value of the UA property. Will be removed in future versions")] + public UserAgent UserAgent => UA; + + // ReSharper disable once InconsistentNaming + /// + /// The User Agent parsed from the user agent string + /// + public UserAgent UA { get; } + + /// + /// Constructs an instance of the ClientInfo with results of the user agent string parsing + /// + public ClientInfo(string inputString, OS os, Device device, UserAgent userAgent) + { + String = inputString; + OS = os; + Device = device; + UA = userAgent; + } + + /// + /// A readable description of the user agent client information + /// + /// + public override string ToString() + { + return $"{OS} {Device} {UA}"; + } + } +} diff --git a/UAParser/Implementation/DeviceParser.cs b/UAParser/Implementation/DeviceParser.cs new file mode 100644 index 0000000..41d58a1 --- /dev/null +++ b/UAParser/Implementation/DeviceParser.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class DeviceParser : AbstractParser + { + public DeviceParser(IEnumerable> maps, ParserOptions options, Device defaultValue = default) + : base(maps, options, defaultValue) + { } + + protected override DeviceTemplate InitializeTemplate(IDictionary map) + { + var regex = Regex(map, "Device", map.Find("regex_flag")); + var device = map.Find("device_replacement"); + var brand = map.Find("brand_replacement"); + var model = map.Find("model_replacement"); + return new DeviceTemplate(regex, device, brand, model); + } + + protected override Device Matcher(string input, DeviceTemplate template) + { + var match = Match(input, template.Regex); + if (match != null && match.Success) + { + using (var num = Generate(1, x => ++x)) + { + var device = ReplaceAll(match, template.DeviceReplacement, num); + var brand = ReplaceAll(match, template.BrandReplacement, num); + var model = ReplaceAll(match, template.ModelReplacement, num); + return new Device(device, brand, model); + } + } + + return default; + } + + private string ReplaceAll(Match m, string replacement, IEnumerator num) + { + if (replacement == null) + return Select(x => x, m, num); + + var finalString = replacement; + if (finalString.Contains("$")) + { + var groups = m.Groups; + for (var i = 0; i < _allReplacementTokens.Length; i++) + { + var tokenNumber = i + 1; + var token = _allReplacementTokens[i]; + if (finalString.Contains(token)) + { + var replacementText = string.Empty; + Group group; + if (tokenNumber <= groups.Count && (group = groups[tokenNumber]).Success) + replacementText = group.Value; + + finalString = ReplaceFunction(finalString, replacementText, token); + } + if (!finalString.Contains("$")) + break; + } + } + + return finalString.Trim(); + } + + private static string ReplaceFunction(string replacementString, string matchedGroup, string token) + { + return matchedGroup != null + ? replacementString.ReplaceFirstOccurence(token, matchedGroup) + : replacementString; + } + } +} diff --git a/UAParser/Implementation/DeviceTemplate.cs b/UAParser/Implementation/DeviceTemplate.cs new file mode 100644 index 0000000..57717f9 --- /dev/null +++ b/UAParser/Implementation/DeviceTemplate.cs @@ -0,0 +1,22 @@ +using System.Text.RegularExpressions; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class DeviceTemplate : Template + { + public DeviceTemplate(Regex regex, string deviceReplacement, string brandReplacement, string modelReplacement) + : base (regex) + { + DeviceReplacement = deviceReplacement; + BrandReplacement = brandReplacement; + ModelReplacement = modelReplacement; + } + + public string DeviceReplacement { get; } + + public string BrandReplacement { get; } + + public string ModelReplacement { get; } + } +} diff --git a/UAParser/Implementation/OSParser.cs b/UAParser/Implementation/OSParser.cs new file mode 100644 index 0000000..8518641 --- /dev/null +++ b/UAParser/Implementation/OSParser.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class OsParser : AbstractParser + { + public OsParser(IEnumerable> maps, ParserOptions options, OS defaultValue = default) + : base (maps, options, defaultValue) + { } + + protected override OsTemplate InitializeTemplate(IDictionary map) + { + var regex = Regex(map, "OS"); + var os = map.Find("os_replacement"); + var v1 = map.Find("os_v1_replacement"); + var v2 = map.Find("os_v2_replacement"); + var v3 = map.Find("os_v3_replacement"); + var v4 = map.Find("os_v4_replacement"); + return new OsTemplate(regex, os, v1, v2, v3, v4); + } + + protected override OS Matcher(string input, OsTemplate template) + { + var match = Match(input, template.Regex); + if (match != null && match.Success) + { + string family = null, major = null, minor = null, patch = null, patchMinor = null; + using (var num = Generate(1, x => ++x)) + { + // For variable replacements to be consistent the order of the linq statements are important ($1 + // is only available to the first 'from X in Replace(..)' and so forth) so a a bit of conditional + // is required to get the creations to work. This is backed by unit tests + if (template.MajorVersionReplacement == "$1") + { + if (template.MinorVersionReplacement == "$2") + { + major = Replace(match, num, template.MajorVersionReplacement, "$1"); + minor = Replace(match, num, template.MinorVersionReplacement, "$2"); + patch = Replace(match, num, template.PatchVersionReplacement, "$3"); + patchMinor = Replace(match, num, template.PatchMinorVersionReplacement, "$4"); + family = Replace(match, num, template.OsNameReplacement, "$5"); + + return new OS(family, major, minor, patch, patchMinor); + } + + major = Replace(match, num, template.MajorVersionReplacement, "$1"); + family = Replace(match, num, template.OsNameReplacement, "$2"); + minor = Replace(match, num, template.MinorVersionReplacement, "$3"); + patch = Replace(match, num, template.PatchVersionReplacement, "$4"); + patchMinor = Replace(match, num, template.PatchMinorVersionReplacement, "$5"); + + return new OS(family, major, minor, patch, patchMinor); + } + + family = Replace(match, num, template.OsNameReplacement, "$1"); + major = Replace(match, num, template.MajorVersionReplacement, "$2"); + minor = Replace(match, num, template.MinorVersionReplacement, "$3"); + patch = Replace(match, num, template.PatchVersionReplacement, "$4"); + patchMinor = Replace(match, num, template.PatchMinorVersionReplacement, "$5"); + + return new OS(family, major, minor, patch, patchMinor); + } + } + + return default; + } + } +} diff --git a/UAParser/Implementation/OsTemplate.cs b/UAParser/Implementation/OsTemplate.cs new file mode 100644 index 0000000..dc364f3 --- /dev/null +++ b/UAParser/Implementation/OsTemplate.cs @@ -0,0 +1,29 @@ +using System.Text.RegularExpressions; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class OsTemplate : Template + { + public OsTemplate(Regex regex, string osNameReplacement, string majorVersionReplacement, string minorVersionReplacement, + string patchVersionReplacement, string patchMinorVersionReplacement) + : base(regex) + { + OsNameReplacement = osNameReplacement; + MajorVersionReplacement = majorVersionReplacement; + MinorVersionReplacement = minorVersionReplacement; + PatchVersionReplacement = patchVersionReplacement; + PatchMinorVersionReplacement = patchMinorVersionReplacement; + } + + public string OsNameReplacement { get; } + + public string MajorVersionReplacement { get; } + + public string MinorVersionReplacement { get; } + + public string PatchVersionReplacement { get; } + + public string PatchMinorVersionReplacement { get; } + } +} diff --git a/UAParser/Implementation/UserAgentParser.cs b/UAParser/Implementation/UserAgentParser.cs new file mode 100644 index 0000000..3d76dc8 --- /dev/null +++ b/UAParser/Implementation/UserAgentParser.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class UserAgentParser : AbstractParser + { + public UserAgentParser(IEnumerable> maps, ParserOptions options, UserAgent defaultValue = default) + : base (maps, options, defaultValue) + { } + + protected override UserAgentTemplate InitializeTemplate(IDictionary map) + { + var regex = Regex(map, "User agent"); + var family = map.Find("family_replacement"); + var v1 = map.Find("v1_replacement"); + var v2 = map.Find("v2_replacement"); + var v3 = map.Find("v3_replacement"); + return new UserAgentTemplate(regex, family, "$1", v1, "$2", v2, "$3", v3, "$4"); + } + + protected override UserAgent Matcher(string input, UserAgentTemplate template) + { + var match = Match(input, template.Regex); + if (match != null && match.Success) + { + using (var num = Generate(1, x => ++x)) + { + var family = Replace(match, num, template.FamilyReplacement, template.FamilyReplacementToken); + var major = Replace(match, num, template.MajorVersionReplacement, template.MajorVersionReplacementToken); + var minor = Replace(match, num, template.MinorVersionReplacement, template.MinorVersionReplacementToken); + var patch = Replace(match, num, template.PatchVersionReplacement, template.PatchVersionReplacementToken); + return new UserAgent(family, major, minor, patch); + } + } + + return default; + } + } +} diff --git a/UAParser/Implementation/UserAgentTemplate.cs b/UAParser/Implementation/UserAgentTemplate.cs new file mode 100644 index 0000000..9d711da --- /dev/null +++ b/UAParser/Implementation/UserAgentTemplate.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; +using UAParser.Abstraction; + +namespace UAParser +{ + internal class UserAgentTemplate : Template + { + public UserAgentTemplate(Regex regex, string familyReplacement, string familyReplacementToken, string majorVersionReplacement, + string majorVersionReplacementToken, string minorVersionReplacement, string minorVersionReplacementToken, string pathVersionReplacement, + string pathVersionReplacementToken) + : base(regex) + { + FamilyReplacement = familyReplacement; + FamilyReplacementToken = familyReplacementToken; + MajorVersionReplacement = majorVersionReplacement; + MajorVersionReplacementToken = majorVersionReplacementToken; + MinorVersionReplacement = minorVersionReplacement; + MinorVersionReplacementToken = minorVersionReplacementToken; + PatchVersionReplacement = pathVersionReplacement; + PatchVersionReplacementToken = pathVersionReplacementToken; + } + + public string FamilyReplacement { get; } + + public string FamilyReplacementToken { get; } + + public string MajorVersionReplacement { get; } + + public string MajorVersionReplacementToken { get; } + + public string MinorVersionReplacement { get; } + + public string MinorVersionReplacementToken { get; } + + public string PatchVersionReplacement { get; } + + public string PatchVersionReplacementToken { get; } + } +} diff --git a/UAParser/MinimalYamlParser.cs b/UAParser/MinimalYamlParser.cs new file mode 100644 index 0000000..a7e4e1c --- /dev/null +++ b/UAParser/MinimalYamlParser.cs @@ -0,0 +1,194 @@ +#region Apache License, Version 2.0 +// +// Copyright 2014 Atif Aziz +// Portions Copyright 2012 Søren Enemærke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +namespace UAParser +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Just enough string parsing to recognize the regexes.yaml file format. Introduced to remove + /// dependency on large Yaml parsing lib. Note that a unittest ensures compatibility + /// by ensuring regexes and properties are read similar to using the full yaml lib + /// + internal class MinimalYamlParser + { + internal class Mapping + { + private Dictionary _lastEntry; + + public Mapping() + { + Sequences = new List>(); + } + + public List> Sequences { get; } + + public void BeginSequence() + { + _lastEntry = new Dictionary(); + Sequences.Add(_lastEntry); + } + + public void AddToSequence(string key, string value) + { + _lastEntry[key] = value; + } + } + + private readonly Dictionary _mappings = new Dictionary(); + + public MinimalYamlParser(string yamlString) + { + using (var yamlStringReader = new StringReader(yamlString)) + ReadIntoMappingModel(yamlStringReader); + } + + public MinimalYamlParser(TextReader yamlReader) + { + ReadIntoMappingModel(yamlReader); + } + + internal IDictionary Mappings => _mappings; + + private void ReadIntoMappingModel(TextReader yamlReader) + { + var lineCount = 0; + Mapping activeMapping = null; + while (yamlReader.Peek() != -1) + { + var line = yamlReader.ReadLine(); + lineCount++; +#if (NETSTANDARD1_0 || NET20 || NET35 || NET40) + MapLine(line, lineCount, ref activeMapping); +#else + MapLine(line.AsSpan(), lineCount, ref activeMapping); +#endif + } + } + +#if (NETSTANDARD1_0 || NET20 || NET35 || NET40) + private void MapLine(string line, int lineCount, ref Mapping activeMapping) + { + if (line.Trim().StartsWith("#")) //skipping comments + return; + if (line.Trim().Length == 0) + return; + + //is this a new mapping entity + if (line[0] != ' ') + { + int indexOfMappingColon = line.IndexOf(':'); + if (indexOfMappingColon == -1) + throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); + string name = line.Substring(0, indexOfMappingColon).Trim(); + activeMapping = new Mapping(); + _mappings.Add(name, activeMapping); + return; + } + + //reading scalar entries into the active mapping + if (activeMapping == null) + throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); + + var seqLine = line.Trim(); + if (seqLine[0] == '-') + { + activeMapping.BeginSequence(); + seqLine = seqLine.Substring(1); + } + + int indexOfColon = seqLine.IndexOf(':'); + if (indexOfColon == -1) + throw new ArgumentException("YamlParsing: Expecting scalar mapping entry to contain a ':', at line " + lineCount); + + string key = seqLine.Substring(0, indexOfColon).Trim(); + string value = ReadQuotedValue(seqLine.Substring(indexOfColon + 1).Trim()); + activeMapping.AddToSequence(key, value); + } + + private static string ReadQuotedValue(string value) + { + if (value.StartsWith("'") && value.EndsWith("'")) + return value.Substring(1, value.Length - 2); + if (value.StartsWith("\"") && value.EndsWith("\"")) + return value.Substring(1, value.Length - 2); + return value; + } +#else + private void MapLine(ReadOnlySpan line, int lineCount, ref Mapping activeMapping) + { + if (line.IsEmpty || line.TrimStart().StartsWith("#".AsSpan(), StringComparison.OrdinalIgnoreCase)) + return; + + //is this a new mapping entity + if (line[0] != ' ') + { + var indexOfMappingColon = line.IndexOf(':'); + if (indexOfMappingColon == -1) + throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); + var name = line.Slice(0, indexOfMappingColon).ToString(); + activeMapping = new Mapping(); + _mappings.Add(name, activeMapping); + return; + } + + //reading scalar entries into the active mapping + if (activeMapping == null) + throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); + + var seqLine = line.Trim(); + if (seqLine[0] == '-') + { + activeMapping.BeginSequence(); + seqLine = seqLine.Slice(1); + } + + var indexOfColon = seqLine.IndexOf(':'); + if (indexOfColon == -1) + throw new ArgumentException("YamlParsing: Expecting scalar mapping entry to contain a ':', at line " + lineCount); + + var key = seqLine.Slice(0, indexOfColon).Trim().ToString(); + var value = ReadQuotedValue(seqLine.Slice(indexOfColon + 1).Trim()); + activeMapping.AddToSequence(key, value); + } + + private static string ReadQuotedValue(ReadOnlySpan value) + { + if (value[0] == '\'' && value.EndsWith("'".AsSpan()) || + value[0] == '"' && value.EndsWith("\"".AsSpan())) + return value.Slice(1, value.Length - 2).ToString(); + return value.ToString(); + } +#endif + + public IEnumerable> ReadMapping(string mappingName) + { + if (_mappings.TryGetValue(mappingName, out var mapping)) + { + foreach (var s in mapping.Sequences) + { + var temp = s; + yield return temp; + } + } + } + } +} diff --git a/UAParser/OS.cs b/UAParser/OS.cs new file mode 100644 index 0000000..369d9cd --- /dev/null +++ b/UAParser/OS.cs @@ -0,0 +1,70 @@ +#region Apache License, Version 2.0 +// +// Copyright 2014 Atif Aziz +// Portions Copyright 2012 Søren Enemærke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +namespace UAParser +{ + /// + /// Represents the operating system the user agent runs on + /// + // ReSharper disable once InconsistentNaming + public sealed class OS : Part + { + /// + /// Constructs an OS instance + /// + public OS(string family, string major, string minor, string patch, string patchMinor) + : base (family) + { + Major = major; + Minor = minor; + Patch = patch; + PatchMinor = patchMinor; + } + + /// + /// The major version of the OS, if available + /// + public string Major { get; } + + /// + /// The minor version of the OS, if available + /// + public string Minor { get; } + + /// + /// The patch version of the OS, if available + /// + public string Patch { get; } + + /// + /// The minor patch version of the OS, if available + /// + public string PatchMinor { get; } + + /// + /// A readable description of the OS + /// + /// + public override string ToString() + { + var version = VersionString.Format(Major, Minor, Patch, PatchMinor); + return Family + (!string.IsNullOrEmpty(version) ? " " + version : null); + } + } +} diff --git a/UAParser/ParserOptions.cs b/UAParser/ParserOptions.cs new file mode 100644 index 0000000..daa7f6f --- /dev/null +++ b/UAParser/ParserOptions.cs @@ -0,0 +1,47 @@ +#region Apache License, Version 2.0 +// +// Copyright 2014 Atif Aziz +// Portions Copyright 2012 Søren Enemærke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +using System; +using System.Text.RegularExpressions; + +namespace UAParser +{ + /// + /// Options available for the parser + /// + public sealed class ParserOptions + { +#if REGEX_COMPILATION + /// + /// If true, will use compiled regular expressions for slower startup time + /// but higher throughput. The default is false. + /// + public bool UseCompiledRegex { get; set; } +#endif + +#if REGEX_MATCHTIMEOUT + /// + /// Allows for specifying the maximum time spent on regular expressions, + /// serving as a fail safe for potential infinite backtracking. The default is + /// set to Regex.InfiniteMatchTimeout + /// + public TimeSpan MatchTimeOut { get; set; } = Regex.InfiniteMatchTimeout; +#endif + } +} diff --git a/UAParser/Part.cs b/UAParser/Part.cs new file mode 100644 index 0000000..b848aae --- /dev/null +++ b/UAParser/Part.cs @@ -0,0 +1,22 @@ +namespace UAParser +{ + /// + /// + /// + public abstract class Part + { + /// + /// + /// + /// + protected Part(string family) + { + Family = family; + } + + /// + /// The family, is available + /// + public string Family { get; } + } +} diff --git a/UAParser/UAParser.cs b/UAParser/UAParser.cs index 75abbf2..7d44fc7 100644 --- a/UAParser/UAParser.cs +++ b/UAParser/UAParser.cs @@ -17,264 +17,12 @@ // #endregion -using System.Reflection; - namespace UAParser { - using System; - using System.Collections.Generic; using System.IO; using System.Linq; - using System.Text.RegularExpressions; - - /// - /// Represents the physical device the user agent is using - /// - public sealed class Device - { - /// - /// Constructs a Device instance - /// - public Device(string family, string brand, string model) - { - Family = family.Trim(); - if (brand != null) - Brand = brand.Trim(); - if (model != null) - Model = model.Trim(); - } - - /// - /// Returns true if the device is likely to be a spider or a bot device - /// - public bool IsSpider => "Spider".Equals(Family, StringComparison.OrdinalIgnoreCase); - - /// - ///The brand of the device - /// - public string Brand { get; } - /// - /// The family of the device, if available - /// - public string Family { get; } - /// - /// The model of the device, if available - /// - public string Model { get; } - - /// - /// A readable description of the device - /// - public override string ToString() - { - return Family; - } - } - - /// - /// Represents the operating system the user agent runs on - /// - // ReSharper disable once InconsistentNaming - public sealed class OS - { - /// - /// Constructs an OS instance - /// - public OS(string family, string major, string minor, string patch, string patchMinor) - { - Family = family; - Major = major; - Minor = minor; - Patch = patch; - PatchMinor = patchMinor; - } - - /// - /// The familiy of the OS - /// - public string Family { get; } - /// - /// The major version of the OS, if available - /// - public string Major { get; } - /// - /// The minor version of the OS, if available - /// - public string Minor { get; } - /// - /// The patch version of the OS, if available - /// - public string Patch { get; } - /// - /// The minor patch version of the OS, if available - /// - public string PatchMinor { get; } - /// - /// A readable description of the OS - /// - /// - public override string ToString() - { - var version = VersionString.Format(Major, Minor, Patch, PatchMinor); - return Family + (!string.IsNullOrEmpty(version) ? " " + version : null); - } - } - - /// - /// Represents a user agent, commonly a browser - /// - public sealed class UserAgent - { - /// - /// Construct a UserAgent instance - /// - public UserAgent(string family, string major, string minor, string patch) - { - Family = family; - Major = major; - Minor = minor; - Patch = patch; - } - - /// - /// The family of user agent - /// - public string Family { get; } - /// - /// Major version of the user agent, if available - /// - public string Major { get; } - /// - /// Minor version of the user agent, if available - /// - public string Minor { get; } - /// - /// Patch version of the user agent, if available - /// - public string Patch { get; } - - /// - /// The user agent as a readbale string - /// - /// - public override string ToString() - { - var version = VersionString.Format(Major, Minor, Patch); - return Family + (!string.IsNullOrEmpty(version) ? " " + version : null); - } - } - - internal static class VersionString - { - public static string Format(params string[] parts) - { - return string.Join(".", parts.Where(v => !String.IsNullOrEmpty(v)).ToArray()); - } - } - - /// - /// Representing the parse results. Structure of this class aligns with the - /// ua-parser-output WebIDL structure defined in this document: https://github.com/ua-parser/uap-core/blob/master/docs/specification.md - /// - public interface IUAParserOutput - { - /// - /// The user agent string, the input for the UAParser - /// - string String { get; } - - /// - /// The OS parsed from the user agent string - /// - // ReSharper disable once InconsistentNaming - OS OS { get; } - /// - /// The Device parsed from the user agent string - /// - Device Device { get; } - // ReSharper disable once InconsistentNaming - /// - /// The User Agent parsed from the user agent string - /// - UserAgent UA { get; } - } - - /// - /// Represents the user agent client information resulting from parsing - /// a user agent string - /// - public class ClientInfo : IUAParserOutput - { - /// - /// The user agent string, the input for the UAParser - /// - public string String { get; } - // ReSharper disable once InconsistentNaming - /// - /// The OS parsed from the user agent string - /// - // ReSharper disable once InconsistentNaming - public OS OS { get; } - - /// - /// The Device parsed from the user agent string - /// - public Device Device { get; } - /// - /// The User Agent parsed from the user agent string - /// - [Obsolete("Mirrors the value of the UA property. Will be removed in future versions")] - public UserAgent UserAgent => UA; - - // ReSharper disable once InconsistentNaming - /// - /// The User Agent parsed from the user agent string - /// - public UserAgent UA { get; } - - /// - /// Constructs an instance of the ClientInfo with results of the user agent string parsing - /// - public ClientInfo(string inputString, OS os, Device device, UserAgent userAgent) - { - String = inputString; - OS = os; - Device = device; - UA = userAgent; - } - - /// - /// A readable description of the user agent client information - /// - /// - public override string ToString() - { - return $"{OS} {Device} {UA}"; - } - } - - /// - /// Options available for the parser - /// - public sealed class ParserOptions - { -#if REGEX_COMPILATION - /// - /// If true, will use compiled regular expressions for slower startup time - /// but higher throughput. The default is false. - /// - public bool UseCompiledRegex { get; set; } -#endif - -#if REGEX_MATCHTIMEOUT - /// - /// Allows for specifying the maximum time spent on regular expressions, - /// serving as a fail safe for potential infinite backtracking. The default is - /// set to Regex.InfiniteMatchTimeout - /// - public TimeSpan MatchTimeOut { get; set; } = Regex.InfiniteMatchTimeout; -#endif - } + using System.Reflection; + using UAParser.Abstraction; /// /// Represents a parser of a user agent string @@ -287,22 +35,19 @@ public sealed class Parser /// public const string Other = "Other"; - private readonly Func _osParser; - private readonly Func _deviceParser; - private readonly Func _userAgentParser; + private readonly IParser _deviceParser; + private readonly IParser _userAgentParser; + private readonly IParser _osParser; + + //private readonly Func _osParser; private Parser(MinimalYamlParser yamlParser, ParserOptions options) { - var config = new Config(options ?? new ParserOptions()); + if (options == null) options = new ParserOptions(); - _userAgentParser = CreateParser(Read(yamlParser.ReadMapping("user_agent_parsers"), config.UserAgentSelector), new UserAgent(Other, null, null, null)); - _osParser = CreateParser(Read(yamlParser.ReadMapping("os_parsers"), config.OSSelector), new OS(Other, null, null, null, null)); - _deviceParser = CreateParser(Read(yamlParser.ReadMapping("device_parsers"), config.DeviceSelector), new Device(Other, string.Empty, string.Empty)); - } - - private static IEnumerable Read(IEnumerable> entries, Func, T> selector) - { - return from cm in entries select selector(cm.Find); + _osParser = new OsParser(yamlParser.ReadMapping("os_parsers"), options, new OS(Other, null, null, null, null)); + _deviceParser = new DeviceParser(yamlParser.ReadMapping("device_parsers"), options, new Device(Other, string.Empty, string.Empty)); + _userAgentParser = new UserAgentParser(yamlParser.ReadMapping("user_agent_parsers"), options, new UserAgent(Other, null, null, null)); } /// @@ -331,7 +76,7 @@ public static Parser GetDefault(ParserOptions parserOptions = null) .Assembly.GetManifestResourceStream("UAParser.regexes.yaml")) // ReSharper disable once AssignNullToNotNullAttribute using (var reader = new StreamReader(stream)) - return new Parser(new MinimalYamlParser(reader.ReadToEnd()), parserOptions); + return new Parser(new MinimalYamlParser(reader), parserOptions); } /// @@ -339,418 +84,36 @@ public static Parser GetDefault(ParserOptions parserOptions = null) /// public ClientInfo Parse(string uaString) { - var os = ParseOS(uaString); + var os = ParseOS(uaString); var device = ParseDevice(uaString); - var ua = ParseUserAgent(uaString); + var ua = ParseUserAgent(uaString); return new ClientInfo(uaString, os, device, ua); } /// /// Parse a user agent string and obtain the OS information /// - public OS ParseOS(string uaString) { return _osParser(uaString); } + public OS ParseOS(string uaString) + => _osParser.Parse(uaString); + /// /// Parse a user agent string and obtain the device information /// - public Device ParseDevice(string uaString) { return _deviceParser(uaString); } + public Device ParseDevice(string uaString) + => _deviceParser.Parse(uaString); + /// /// Parse a user agent string and obtain the UserAgent information /// - public UserAgent ParseUserAgent(string uaString) { return _userAgentParser(uaString); } - - private static Func CreateParser(IEnumerable> parsers, T defaultValue) where T : class - { - return CreateParser(parsers, defaultValue, t => t); - } - - private static Func CreateParser(IEnumerable> parsers, T defaultValue, Func selector) where T : class - { - parsers = parsers?.ToArray() ?? Enumerable.Empty>(); - return ua => selector(parsers.Select(p => p(ua)).FirstOrDefault(m => m != null) ?? defaultValue); - } - - private class Config - { - private readonly ParserOptions _options; - - internal Config(ParserOptions options) - { - _options = options; - } - - // ReSharper disable once InconsistentNaming - public Func OSSelector(Func indexer) - { - var regex = Regex(indexer, "OS"); - var os = indexer("os_replacement"); - var v1 = indexer("os_v1_replacement"); - var v2 = indexer("os_v2_replacement"); - var v3 = indexer("os_v3_replacement"); - var v4 = indexer("os_v4_replacement"); - return Parsers.OS(regex, os, v1, v2, v3, v4); - } - - public Func UserAgentSelector(Func indexer) - { - var regex = Regex(indexer, "User agent"); - var family = indexer("family_replacement"); - var v1 = indexer("v1_replacement"); - var v2 = indexer("v2_replacement"); - var v3 = indexer("v3_replacement"); - return Parsers.UserAgent(regex, family, v1, v2, v3); - } - - public Func DeviceSelector(Func indexer) - { - var regex = Regex(indexer, "Device", indexer("regex_flag")); - var device = indexer("device_replacement"); - var brand = indexer("brand_replacement"); - var model = indexer("model_replacement"); - return Parsers.Device(regex, device, brand, model); - } - - private Regex Regex(Func indexer, string key, string regexFlag = null) - { - var pattern = indexer("regex"); - if (pattern == null) - throw new Exception($"{key} is missing regular expression specification."); - - // Some expressions in the regex.yaml file causes parsing errors - // in .NET such as the \_ token so need to alter them before - // proceeding. - - if (pattern.IndexOf(@"\_", StringComparison.Ordinal) >= 0) - pattern = pattern.Replace(@"\_", "_"); - - //Singleline: User agent strings do not contain newline characters. RegexOptions.Singleline improves performance. - //CultureInvariant: The interpretation of a user agent never depends on the current locale. - RegexOptions options = RegexOptions.Singleline | RegexOptions.CultureInvariant; - - if ("i".Equals(regexFlag)) - { - options |= RegexOptions.IgnoreCase; - } - -#if REGEX_COMPILATION - if (_options.UseCompiledRegex) - { - options |= RegexOptions.Compiled; - } -#endif - -#if REGEX_MATCHTIMEOUT - - return new Regex(pattern, options, _options.MatchTimeOut); -#else - return new Regex(pattern, options); -#endif - } - } - - private static class Parsers - { - // ReSharper disable once InconsistentNaming - public static Func OS(Regex regex, string osReplacement, string v1Replacement, string v2Replacement, string v3Replacement, string v4Replacement) - { - // For variable replacements to be consistent the order of the linq statements are important ($1 - // is only available to the first 'from X in Replace(..)' and so forth) so a a bit of conditional - // is required to get the creations to work. This is backed by unit tests - if (v1Replacement == "$1") - { - if (v2Replacement == "$2") - { - return Create(regex, from v1 in Replace(v1Replacement, "$1") - from v2 in Replace(v2Replacement, "$2") - from v3 in Replace(v3Replacement, "$3") - from v4 in Replace(v4Replacement, "$4") - from family in Replace(osReplacement, "$5") - select new OS(family, v1, v2, v3, v4)); - } - - return Create(regex, from v1 in Replace(v1Replacement, "$1") - from family in Replace(osReplacement, "$2") - from v2 in Replace(v2Replacement, "$3") - from v3 in Replace(v3Replacement, "$4") - from v4 in Replace(v4Replacement, "$5") - select new OS(family, v1, v2, v3, v4)); - } - - return Create(regex, from family in Replace(osReplacement, "$1") - from v1 in Replace(v1Replacement, "$2") - from v2 in Replace(v2Replacement, "$3") - from v3 in Replace(v3Replacement, "$4") - from v4 in Replace(v4Replacement, "$5") - select new OS(family, v1, v2, v3, v4)); - } - - public static Func Device(Regex regex, string familyReplacement, string brandReplacement, string modelReplacement) - { - return Create(regex, from family in ReplaceAll(familyReplacement) - from brand in ReplaceAll(brandReplacement) - from model in ReplaceAll(modelReplacement) - select new Device(family, brand, model)); - } - - public static Func UserAgent(Regex regex, string familyReplacement, string majorReplacement, string minorReplacement, string patchReplacement) - { - return Create(regex, from family in Replace(familyReplacement, "$1") - from v1 in Replace(majorReplacement, "$2") - from v2 in Replace(minorReplacement, "$3") - from v3 in Replace(patchReplacement, "$4") - select new UserAgent(family, v1, v2, v3)); - } - - private static Func, string> Replace(string replacement) - { - return replacement != null ? Select(_ => replacement) : Select(); - } - - private static Func, string> Replace( - string replacement, string token) - { - return replacement != null && replacement.Contains(token) - ? Select(s => s != null ? replacement.ReplaceFirstOccurence(token, s) : replacement) - : Replace(replacement); - } - - private static readonly string[] _allReplacementTokens = new string[] - { - "$1","$2","$3","$4","$5","$6","$7","$8","$91", - }; - - private static Func, string> ReplaceAll(string replacement) - { - if (replacement == null) - return Select(); - - string ReplaceFunction(string replacementString, string matchedGroup, string token) - { - return matchedGroup != null - ? replacementString.ReplaceFirstOccurence(token, matchedGroup) - : replacementString; - } - - return (m, num) => - { - var finalString = replacement; - if (finalString.Contains("$")) - { - var groups = m.Groups; - for (int i = 0; i < _allReplacementTokens.Length; i++) - { - int tokenNumber = i + 1; - string token = _allReplacementTokens[i]; - if (finalString.Contains(token)) - { - var replacementText = string.Empty; - Group group; - if (tokenNumber <= groups.Count && (group = groups[tokenNumber]).Success) - replacementText = group.Value; - - finalString = ReplaceFunction(finalString, replacementText, token); - } - if (!finalString.Contains("$")) - break; - } - } - return finalString; - }; - } - - private static Func, string> Select() - { - return Select(v => v); - } - - private static Func, T> Select(Func selector) - { - return (m, num) => - { - if (!num.MoveNext()) throw new InvalidOperationException(); - var groups = m.Groups; Group group; - return selector(num.Current <= groups.Count && (group = groups[num.Current]).Success - ? group.Value : null); - }; - } - - private static Func Create(Regex regex, Func, T> binder) - { - return input => - { -#if REGEX_MATCHTIMEOUT - try - { - var m = regex.Match(input); - var num = Generate(1, n => n + 1); - return m.Success ? binder(m, num) : default(T); - } - catch (RegexMatchTimeoutException) - { - // we'll simply swallow this exception and return the default (non-matched) - return default(T); - } -#else - var m = regex.Match(input); - var num = Generate(1, n => n + 1); - return m.Success ? binder(m, num) : default(T); -#endif - }; - } - - private static IEnumerator Generate(T initial, Func next) - { - for (var state = initial; ; state = next(state)) - yield return state; - // ReSharper disable once FunctionNeverReturns - } - } - } - - internal static class RegexBinderBuilder - { - public static Func, TResult> SelectMany( - this Func, T1> binder, - Func, T2>> continuation, - Func projection) - { - return (m, num) => - { - T1 bound = binder(m, num); - T2 continued = continuation(bound)(m, num); - TResult projected = projection(bound, continued); - return projected; - }; - } - } - - internal static class StringExtensions - { - public static string ReplaceFirstOccurence(this string input, string search, string replacement) - { - if (input == null) throw new ArgumentNullException(nameof(input)); - var index = input.IndexOf(search, StringComparison.Ordinal); - return index >= 0 - ? input.Substring(0, index) + replacement + input.Substring(index + search.Length) - : input; - } + public UserAgent ParseUserAgent(string uaString) + => _userAgentParser.Parse(uaString); } - internal static class DictionaryExtensions - { - public static TValue Find(this IDictionary dictionary, TKey key) - { - if (dictionary == null) throw new ArgumentNullException(nameof(dictionary)); - return dictionary.TryGetValue(key, out var result) ? result : default(TValue); - } - } - - /// - /// Just enough string parsing to recognize the regexes.yaml file format. Introduced to remove - /// dependency on large Yaml parsing lib. Note that a unittest ensures compatibility - /// by ensuring regexes and properties are read similar to using the full yaml lib - /// - internal class MinimalYamlParser + internal static class VersionString { - internal class Mapping - { - private Dictionary _lastEntry; - - public Mapping() - { - Sequences = new List>(); - } - - public List> Sequences { get; } - - public void BeginSequence() - { - _lastEntry = new Dictionary(); - Sequences.Add(_lastEntry); - } - - public void AddToSequence(string key, string value) - { - _lastEntry[key] = value; - } - } - - private readonly Dictionary _mappings = new Dictionary(); - - public MinimalYamlParser(string yamlString) - { - ReadIntoMappingModel(yamlString); - } - - internal IDictionary Mappings => _mappings; - - private void ReadIntoMappingModel(string yamlInputString) - { - // line splitting using various splitting characters - string[] lines = yamlInputString.Split(new[] { Environment.NewLine, "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries); - int lineCount = 0; - Mapping activeMapping = null; - - foreach (var line in lines) - { - lineCount++; - if (line.Trim().StartsWith("#")) //skipping comments - continue; - if (line.Trim().Length == 0) - continue; - - //is this a new mapping entity - if (line[0] != ' ') - { - int indexOfMappingColon = line.IndexOf(':'); - if (indexOfMappingColon == -1) - throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); - string name = line.Substring(0, indexOfMappingColon).Trim(); - activeMapping = new Mapping(); - _mappings.Add(name, activeMapping); - continue; - } - - //reading scalar entries into the active mapping - if (activeMapping == null) - throw new ArgumentException("YamlParsing: Expecting mapping entry to contain a ':', at line " + lineCount); - - var seqLine = line.Trim(); - if (seqLine[0] == '-') - { - activeMapping.BeginSequence(); - seqLine = seqLine.Substring(1); - } - - int indexOfColon = seqLine.IndexOf(':'); - if (indexOfColon == -1) - throw new ArgumentException("YamlParsing: Expecting scalar mapping entry to contain a ':', at line " + lineCount); - - string key = seqLine.Substring(0, indexOfColon).Trim(); - string value = ReadQuotedValue(seqLine.Substring(indexOfColon + 1).Trim()); - activeMapping.AddToSequence(key, value); - } - } - - private static string ReadQuotedValue(string value) - { - if (value.StartsWith("'") && value.EndsWith("'")) - return value.Substring(1, value.Length - 2); - if (value.StartsWith("\"") && value.EndsWith("\"")) - return value.Substring(1, value.Length - 2); - return value; - } - - public IEnumerable> ReadMapping(string mappingName) + public static string Format(params string[] parts) { - if (_mappings.TryGetValue(mappingName, out var mapping)) - { - foreach (var s in mapping.Sequences) - { - var temp = s; - yield return temp; - } - } + return string.Join(".", parts.Where(v => !string.IsNullOrEmpty(v)).ToArray()); } } - } diff --git a/UAParser/UAParser.csproj b/UAParser/UAParser.csproj index a8eab6c..f82f960 100644 --- a/UAParser/UAParser.csproj +++ b/UAParser/UAParser.csproj @@ -1,7 +1,7 @@  - netstandard1.0;netstandard1.3;netstandard1.6;net20;netcoreapp2.0;net35;net40;net45 + netstandard1.0;netstandard1.3;netstandard1.6;netstandard2.0;net20;netcoreapp2.0;netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net35;net40;net45 UAParser enemaerke User Agent Parser for .Net @@ -37,14 +37,30 @@ REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT - - REGEX_COMPILATION + + REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT - + REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT + + REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT + + + + REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT + + + + REGEX_COMPILATION;INTROSPECTION_EXTENSIONS;REGEX_MATCHTIMEOUT + + + + REGEX_COMPILATION + + REGEX_COMPILATION @@ -79,4 +95,52 @@ + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + + + + 4.5.4 + + + diff --git a/UAParser/UserAgent.cs b/UAParser/UserAgent.cs new file mode 100644 index 0000000..e670dbf --- /dev/null +++ b/UAParser/UserAgent.cs @@ -0,0 +1,63 @@ +#region Apache License, Version 2.0 +// +// Copyright 2014 Atif Aziz +// Portions Copyright 2012 Søren Enemærke +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#endregion + +namespace UAParser +{ + /// + /// Represents a user agent, commonly a browser + /// + public sealed class UserAgent : Part + { + /// + /// Construct a UserAgent instance + /// + public UserAgent(string family, string major, string minor, string patch) + : base (family) + { + Major = major; + Minor = minor; + Patch = patch; + } + + /// + /// Major version of the user agent, if available + /// + public string Major { get; } + + /// + /// Minor version of the user agent, if available + /// + public string Minor { get; } + + /// + /// Patch version of the user agent, if available + /// + public string Patch { get; } + + /// + /// The user agent as a readbale string + /// + /// + public override string ToString() + { + var version = VersionString.Format(Major, Minor, Patch); + return Family + (!string.IsNullOrEmpty(version) ? " " + version : null); + } + } +} From 36891dd79b05ddfab1eaba8c60be3ffa61429685 Mon Sep 17 00:00:00 2001 From: Eugene Smelov Date: Tue, 5 May 2020 21:23:24 +0300 Subject: [PATCH 2/2] cleanup --- UAParser.ConsoleApp/Program.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/UAParser.ConsoleApp/Program.cs b/UAParser.ConsoleApp/Program.cs index 7fb3ede..e3c9b47 100644 --- a/UAParser.ConsoleApp/Program.cs +++ b/UAParser.ConsoleApp/Program.cs @@ -13,8 +13,6 @@ static void Main(string[] args) return; } - Console.ReadKey(); - var uaParser = Parser.GetDefault(); string uaString; while ((uaString = Console.In.ReadLine()) != null) @@ -22,7 +20,6 @@ static void Main(string[] args) uaString = uaString.Trim(); if (uaString.Length == 0) continue; - Console.ReadKey(); var c = uaParser.Parse(uaString); Console.WriteLine("Agent : {0}", c.UA); Console.WriteLine("OS : {0}", c.OS);