using System.Collections.Generic; using System.Text.RegularExpressions; namespace EmbedIO.Utilities { /// /// Represents a list of names with associated quality values extracted from an HTTP header, /// e.g. gzip; q=0.9, deflate. /// See RFC7231, section 5.3. /// This class ignores and discards extensions (accept-ext in RFC7231 terminology). /// If a name has one or more parameters (e.g. text/html;level=1) it is not /// further parsed: parameters will appear as part of the name. /// public sealed class QValueList { /// /// A value signifying "anything will do" in request headers. /// For example, a request header of /// Accept-Encoding: *;q=0.8, gzip means "I prefer GZip compression; /// if it is not available, any other compression (including no compression at all) /// is OK for me". /// public const string Wildcard = "*"; // This will match a quality value between two semicolons // or between a semicolon and the end of a string. // Match groups will be: // Groups[0] = The matching string // Groups[1] = If group is successful, "0"; otherwise, the weight is 1.000 // Groups[2] = If group is successful, the decimal digits after 0 // The part of string before the match contains the value and parameters (if any). // The part of string after the match contains the extensions (if any). // If there is no match, the whole string is just value and parameters (if any). private static readonly Regex QualityValueRegex = new Regex( @";[ \t]*q=(?:(?:1(?:\.(?:0{1,3}))?)|(?:(0)(?:\.(\d{1,3}))?))[ \t]*(?:;|,|$)", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); /// /// Initializes a new instance of the class /// by parsing comma-separated request header values. /// /// If set to , a value of * /// will be treated as signifying "anything". /// A list of comma-separated header values. /// public QValueList(bool useWildcard, string headerValues) { UseWildcard = useWildcard; QValues = Parse(headerValues); } /// /// Initializes a new instance of the class /// by parsing comma-separated request header values. /// /// If set to , a value of * /// will be treated as signifying "anything". /// An enumeration of header values. /// Note that each element of the enumeration may in turn be /// a comma-separated list. /// public QValueList(bool useWildcard, IEnumerable headerValues) { UseWildcard = useWildcard; QValues = Parse(headerValues); } /// /// Initializes a new instance of the class /// by parsing comma-separated request header values. /// /// If set to , a value of * /// will be treated as signifying "anything". /// An array of header values. /// Note that each element of the array may in turn be /// a comma-separated list. /// public QValueList(bool useWildcard, params string[] headerValues) : this(useWildcard, headerValues as IEnumerable) { } /// /// Gets a dictionary associating values with their relative weight /// (an integer ranging from 0 to 1000) and their position in the /// list of header values from which this instance has been constructed. /// /// /// This property does not usually need to be used directly; /// use the , , /// , and /// methods instead. /// /// /// /// /// public IReadOnlyDictionary QValues { get; } /// /// Gets a value indicating whether * is treated as a special value /// with the meaning of "anything". /// public bool UseWildcard { get; } /// /// Determines whether the specified value is a possible candidate. /// /// The value. /// if is a candidate; /// otherwise, . public bool IsCandidate(string value) => TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate) && candidate.Weight > 0; /// /// Attempts to determine whether the weight of a possible candidate. /// /// The value whose weight is to be determined. /// When this method returns , /// the weight of the candidate. /// if is a candidate; /// otherwise, . public bool TryGetWeight(string value, out int weight) { var result = TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate); weight = candidate.Weight; return result; } /// /// Finds the value preferred by the client among an enumeration of values. /// /// The values. /// The value preferred by the client, or /// if none of the provided is accepted. public string? FindPreferred(IEnumerable values) => FindPreferredCore(values, out var result) >= 0 ? result : null; /// /// Finds the index of the value preferred by the client in a list of values. /// /// The values. /// The index of the value preferred by the client, or -1 /// if none of the values in is accepted. public int FindPreferredIndex(IEnumerable values) => FindPreferredCore(values, out _); /// /// Finds the index of the value preferred by the client in an array of values. /// /// The values. /// The index of the value preferred by the client, or -1 /// if none of the values in is accepted. public int FindPreferredIndex(params string[] values) => FindPreferredIndex(values as IReadOnlyList); private static IReadOnlyDictionary Parse(string headerValues) { var result = new Dictionary(); ParseCore(headerValues, result); return result; } private static IReadOnlyDictionary Parse(IEnumerable headerValues) { var result = new Dictionary(); if (headerValues == null) return result; foreach (var headerValue in headerValues) ParseCore(headerValue, result); return result; } private static void ParseCore(string text, IDictionary dictionary) { if (string.IsNullOrEmpty(text)) return; var length = text.Length; var position = 0; var ordinal = 0; while (position < length) { var stop = text.IndexOf(',', position); if (stop < 0) stop = length; string name; var weight = 1000; var match = QualityValueRegex.Match(text, position, stop - position); if (match.Success) { var groups = match.Groups; var wholeMatch = groups[0]; name = text.Substring(position, wholeMatch.Index - position).Trim(); if (groups[1].Success) { weight = 0; if (groups[2].Success) { var digits = groups[2].Value; var n = 0; while (n < digits.Length) { weight = (10 * weight) + (digits[n] - '0'); n++; } while (n < 3) { weight = 10 * weight; n++; } } } } else { name = text.Substring(position, stop - position).Trim(); } if (!string.IsNullOrEmpty(name)) dictionary[name] = (weight, ordinal); position = stop + 1; ordinal++; } } private static int CompareQualities((int Weight, int Ordinal) a, (int Weight, int Ordinal) b) { if (a.Weight > b.Weight) return 1; if (a.Weight < b.Weight) return -1; if (a.Ordinal < b.Ordinal) return 1; if (a.Ordinal > b.Ordinal) return -1; return 0; } private int FindPreferredCore(IEnumerable values, out string? result) { values = Validate.NotNull(nameof(values), values); result = null; var best = -1; // Set initial values such as a weight of 0 can never win over them (int Weight, int Ordinal) bestValue = (0, int.MinValue); var i = 0; foreach (var value in values) { if (value == null) continue; if (TryGetCandidateValue(value, out var candidateValue) && CompareQualities(candidateValue, bestValue) > 0) { result = value; best = i; bestValue = candidateValue; } i++; } return best; } private bool TryGetCandidateValue(string value, out (int Weight, int Ordinal) candidate) => QValues.TryGetValue(value, out candidate) || (UseWildcard && QValues.TryGetValue(Wildcard, out candidate)); } }