diff --git a/UAParser.ConsoleApp/Program.cs b/UAParser.ConsoleApp/Program.cs index c84bef5..e3c9b47 100644 --- a/UAParser.ConsoleApp/Program.cs +++ b/UAParser.ConsoleApp/Program.cs @@ -1,34 +1,34 @@ 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); - } - } + 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); + } + } - static void Help() - { + static void Help() + { Console.WriteLine(@"UAParser Copyright 2015 " + "S\u00f8ren Enem\u00e6rke" + @" https://github.com/tobie/ua-parser @@ -36,6 +36,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.Tests/DeviceYamlTestCase.cs b/UAParser.Tests/DeviceYamlTestCase.cs index d210c8e..4e4a65c 100644 --- a/UAParser.Tests/DeviceYamlTestCase.cs +++ b/UAParser.Tests/DeviceYamlTestCase.cs @@ -2,94 +2,95 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using UAParser.Implementations; using Xunit; namespace UAParser.Tests { - public class DeviceYamlTestCase : YamlTestCase - { - public static DeviceYamlTestCase ReadFromMap(Dictionary map) + public class DeviceYamlTestCase : YamlTestCase { - DeviceYamlTestCase tc = new DeviceYamlTestCase() - { - UserAgent = map["user_agent_string"], - Family = map["family"], + public static DeviceYamlTestCase ReadFromMap(Dictionary map) + { + DeviceYamlTestCase tc = new DeviceYamlTestCase() + { + UserAgent = map["user_agent_string"], + Family = map["family"], - }; - return tc; - } + }; + return tc; + } - public string Family { get; set; } + public string Family { get; set; } - public override void Verify(ClientInfo clientInfo) - { - Assert.NotNull(clientInfo); - AssertMatch(Family,clientInfo.Device.Family,"Family"); + public override void Verify(ClientInfo clientInfo) + { + Assert.NotNull(clientInfo); + AssertMatch(Family, clientInfo.Device.Family, "Family"); + } } - } - public class OSYamlTestCase : YamlTestCase - { - public static OSYamlTestCase ReadFromMap(Dictionary map) + public class OSYamlTestCase : YamlTestCase { - OSYamlTestCase tc = new OSYamlTestCase() - { - UserAgent = map["user_agent_string"], - Family = map["family"], - Major = map["major"], - Minor = map["minor"], - Patch = map["patch"], - PatchMinor = map["patch_minor"] - }; - return tc; - } + public static OSYamlTestCase ReadFromMap(Dictionary map) + { + OSYamlTestCase tc = new OSYamlTestCase() + { + UserAgent = map["user_agent_string"], + Family = map["family"], + Major = map["major"], + Minor = map["minor"], + Patch = map["patch"], + PatchMinor = map["patch_minor"] + }; + return tc; + } - public string Family { get; set; } - public string Major { get; set; } - public string Minor { get; set; } - public string Patch { get; set; } - public string PatchMinor { get; set; } + public string Family { get; set; } + public string Major { get; set; } + public string Minor { get; set; } + public string Patch { get; set; } + public string PatchMinor { get; set; } - public override void Verify(ClientInfo clientInfo) - { - Assert.NotNull(clientInfo); - AssertMatch(Family, clientInfo.OS.Family, "Family"); - AssertMatch(Major, clientInfo.OS.Major, "Major"); - AssertMatch(Minor, clientInfo.OS.Minor, "Minor"); - AssertMatch(Patch, clientInfo.OS.Patch, "Patch"); - AssertMatch(PatchMinor, clientInfo.OS.PatchMinor, "PatchMinor"); + public override void Verify(ClientInfo clientInfo) + { + Assert.NotNull(clientInfo); + AssertMatch(Family, clientInfo.OS.Family, "Family"); + AssertMatch(Major, clientInfo.OS.Major, "Major"); + AssertMatch(Minor, clientInfo.OS.Minor, "Minor"); + AssertMatch(Patch, clientInfo.OS.Patch, "Patch"); + AssertMatch(PatchMinor, clientInfo.OS.PatchMinor, "PatchMinor"); + } } - } - public class UserAgentYamlTestCase : YamlTestCase - { - public static UserAgentYamlTestCase ReadFromMap(Dictionary map) + public class UserAgentYamlTestCase : YamlTestCase { - UserAgentYamlTestCase tc = new UserAgentYamlTestCase() - { - UserAgent = map["user_agent_string"], - Family = map["family"], - Major = map["major"], - Minor = map["minor"], - Patch = map["patch"], - }; - return tc; - } + public static UserAgentYamlTestCase ReadFromMap(Dictionary map) + { + UserAgentYamlTestCase tc = new UserAgentYamlTestCase() + { + UserAgent = map["user_agent_string"], + Family = map["family"], + Major = map["major"], + Minor = map["minor"], + Patch = map["patch"], + }; + return tc; + } - public string Family { get; set; } - public string Major { get; set; } - public string Minor { get; set; } - public string Patch { get; set; } + public string Family { get; set; } + public string Major { get; set; } + public string Minor { get; set; } + public string Patch { get; set; } - public override void Verify(ClientInfo clientInfo) - { - Assert.NotNull(clientInfo); - AssertMatch(Family, clientInfo.UA.Family, "Family"); - AssertMatch(Major, clientInfo.UA.Major, "Major"); - AssertMatch(Minor, clientInfo.UA.Minor, "Minor"); - AssertMatch(Patch, clientInfo.UA.Patch, "Patch"); + public override void Verify(ClientInfo clientInfo) + { + Assert.NotNull(clientInfo); + AssertMatch(Family, clientInfo.UA.Family, "Family"); + AssertMatch(Major, clientInfo.UA.Major, "Major"); + AssertMatch(Minor, clientInfo.UA.Minor, "Minor"); + AssertMatch(Patch, clientInfo.UA.Patch, "Patch"); + } } - } } diff --git a/UAParser.Tests/InternalExtensions.cs b/UAParser.Tests/InternalExtensions.cs index 23bce37..4d669b5 100644 --- a/UAParser.Tests/InternalExtensions.cs +++ b/UAParser.Tests/InternalExtensions.cs @@ -20,12 +20,12 @@ internal static List> ConvertToDictionaryList(this Ya } internal static Dictionary ConvertToDictionary(this YamlMappingNode yamlNode) { - Dictionary dic = new Dictionary(); - foreach (var key in yamlNode.Children.Keys) - { - dic[key.ToString()] = yamlNode.Children[key].ToString(); - } - return dic; + Dictionary dic = new Dictionary(); + foreach (var key in yamlNode.Children.Keys) + { + dic[key.ToString()] = yamlNode.Children[key].ToString(); + } + return dic; } internal static string GetTestResources(this object self, string name) { diff --git a/UAParser.Tests/TestResourceTests.cs b/UAParser.Tests/TestResourceTests.cs index 3d5d8dc..d68036a 100644 --- a/UAParser.Tests/TestResourceTests.cs +++ b/UAParser.Tests/TestResourceTests.cs @@ -106,7 +106,7 @@ public List GetTestCases( .Select(configMap => { if (!configMap.ContainsKey("js_ua")) //we deliberately skip tests with js-user agents - return testCaseFunction(configMap); + return testCaseFunction(configMap); return default(TTestCase); }) .ToList(); diff --git a/UAParser.Tests/YamlParsing.cs b/UAParser.Tests/YamlParsing.cs index 80bf7d2..b89e616 100644 --- a/UAParser.Tests/YamlParsing.cs +++ b/UAParser.Tests/YamlParsing.cs @@ -43,15 +43,15 @@ from e in rn.Children { var configNode = kvPair.Value; var valueDic = from node in configNode ?? Enumerable.Empty() - select node as YamlMappingNode + select node as YamlMappingNode into node - where node != null - select node.Children - .Where(e => e.Key is YamlScalarNode && e.Value is YamlScalarNode) - .GroupBy(e => e.Key.ToString(), e => e.Value.ToString(), StringComparer.OrdinalIgnoreCase) - .ToDictionary(e => e.Key, e => e.Last(), StringComparer.OrdinalIgnoreCase) + where node != null + select node.Children + .Where(e => e.Key is YamlScalarNode && e.Value is YamlScalarNode) + .GroupBy(e => e.Key.ToString(), e => e.Value.ToString(), StringComparer.OrdinalIgnoreCase) + .ToDictionary(e => e.Key, e => e.Last(), StringComparer.OrdinalIgnoreCase) into cm - select cm; + select cm; string name = kvPair.Key; var minimalLookupList = minimal.ReadMapping(name).ToList(); diff --git a/UAParser.Tests/YamlTestCase.cs b/UAParser.Tests/YamlTestCase.cs index f8e114a..64b4535 100644 --- a/UAParser.Tests/YamlTestCase.cs +++ b/UAParser.Tests/YamlTestCase.cs @@ -2,27 +2,28 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using UAParser.Implementations; using Xunit; namespace UAParser.Tests { - public abstract class YamlTestCase - { - public string UserAgent { get; set; } - public abstract void Verify(ClientInfo clientInfo); - - protected void AssertMatch(T expected, T actual, string type) + public abstract class YamlTestCase { - if (typeof(T) == typeof(string)) - { - string exp = expected as string; - string act = actual as string; + public string UserAgent { get; set; } + public abstract void Verify(ClientInfo clientInfo); + + protected void AssertMatch(T expected, T actual, string type) + { + if (typeof(T) == typeof(string)) + { + string exp = expected as string; + string act = actual as string; - if (string.IsNullOrEmpty(exp) && string.IsNullOrEmpty(act)) - return; - } + if (string.IsNullOrEmpty(exp) && string.IsNullOrEmpty(act)) + return; + } - Assert.True(expected.Equals(actual), type+" did not match. (expected:" + expected + " actual:" + actual + ") in " + UserAgent); + Assert.True(expected.Equals(actual), type + " did not match. (expected:" + expected + " actual:" + actual + ") in " + UserAgent); + } } - } } diff --git a/UAParser/Abstractions/IUAParserOutput.cs b/UAParser/Abstractions/IUAParserOutput.cs new file mode 100644 index 0000000..e74d351 --- /dev/null +++ b/UAParser/Abstractions/IUAParserOutput.cs @@ -0,0 +1,50 @@ +#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.Abstractions +{ + /// + /// 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/Device.cs b/UAParser/Device.cs new file mode 100644 index 0000000..7e4714b --- /dev/null +++ b/UAParser/Device.cs @@ -0,0 +1,69 @@ +#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 + { + /// + /// 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; + } + } + +} diff --git a/UAParser/Extensions/DictionaryExtensions.cs b/UAParser/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..5716862 --- /dev/null +++ b/UAParser/Extensions/DictionaryExtensions.cs @@ -0,0 +1,35 @@ +#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.Extensions +{ + using System; + using System.Collections.Generic; + + 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; + } + } + +} diff --git a/UAParser/Extensions/StringExtensions.cs b/UAParser/Extensions/StringExtensions.cs new file mode 100644 index 0000000..45847b6 --- /dev/null +++ b/UAParser/Extensions/StringExtensions.cs @@ -0,0 +1,37 @@ +#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.Extensions +{ + using System; + + 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; + } + } + +} diff --git a/UAParser/Implementations/ClientInfo.cs b/UAParser/Implementations/ClientInfo.cs new file mode 100644 index 0000000..5bd2d1e --- /dev/null +++ b/UAParser/Implementations/ClientInfo.cs @@ -0,0 +1,80 @@ +#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.Implementations +{ + using System; + using UAParser.Abstractions; + + /// + /// 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/MinimalYamlParser.cs b/UAParser/MinimalYamlParser.cs new file mode 100644 index 0000000..03f6498 --- /dev/null +++ b/UAParser/MinimalYamlParser.cs @@ -0,0 +1,135 @@ +#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; + + /// + /// 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) + { + 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) + { + 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..2509af8 --- /dev/null +++ b/UAParser/OS.cs @@ -0,0 +1,72 @@ +#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 + { + /// + /// 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); + } + } + +} diff --git a/UAParser/ParserOptions.cs b/UAParser/ParserOptions.cs new file mode 100644 index 0000000..ac3fc1d --- /dev/null +++ b/UAParser/ParserOptions.cs @@ -0,0 +1,49 @@ +#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.Text.RegularExpressions; + + /// + /// 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/RegexBinderBuilder.cs b/UAParser/RegexBinderBuilder.cs new file mode 100644 index 0000000..04e14e2 --- /dev/null +++ b/UAParser/RegexBinderBuilder.cs @@ -0,0 +1,44 @@ +#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.Text.RegularExpressions; + + 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; + }; + } + } + +} diff --git a/UAParser/UAParser.cs b/UAParser/UAParser.cs index 75abbf2..a17dc2a 100644 --- a/UAParser/UAParser.cs +++ b/UAParser/UAParser.cs @@ -26,255 +26,8 @@ namespace UAParser 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 UAParser.Extensions; + using UAParser.Implementations; /// /// Represents a parser of a user agent string @@ -339,9 +92,9 @@ 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); } @@ -460,19 +213,19 @@ public static Func OS(Regex regex, string osReplacement, string v1Re 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)); + 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)); + 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") @@ -594,7 +347,7 @@ private static Func Create(Regex regex, Func n + 1); return m.Success ? binder(m, num) : default(T); #endif - }; + }; } private static IEnumerator Generate(T initial, Func next) @@ -605,152 +358,4 @@ private static IEnumerator Generate(T initial, Func next) } } } - - 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; - } - } - - 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 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) - { - if (_mappings.TryGetValue(mappingName, out var mapping)) - { - foreach (var s in mapping.Sequences) - { - var temp = s; - yield return temp; - } - } - } - } - } diff --git a/UAParser/UserAgent.cs b/UAParser/UserAgent.cs new file mode 100644 index 0000000..1789e41 --- /dev/null +++ b/UAParser/UserAgent.cs @@ -0,0 +1,67 @@ +#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 + { + /// + /// 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); + } + } + +} diff --git a/UAParser/VersionString.cs b/UAParser/VersionString.cs new file mode 100644 index 0000000..553f971 --- /dev/null +++ b/UAParser/VersionString.cs @@ -0,0 +1,34 @@ +#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.Linq; + + internal static class VersionString + { + public static string Format(params string[] parts) + { + return string.Join(".", parts.Where(v => !String.IsNullOrEmpty(v)).ToArray()); + } + } + +}