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