Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data

This commit is contained in:
2026-01-14 22:11:11 +01:00
parent 40a8431464
commit 3f7122d30a
350 changed files with 41444 additions and 119 deletions

View File

@@ -0,0 +1,20 @@
using System;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides extension methods for types implementing <see cref="IComponentCollection{T}"/>.
/// </summary>
public static class ComponentCollectionExtensions
{
/// <summary>
/// Adds the specified component to a collection, without giving it a name.
/// </summary>
/// <typeparam name="T">The type of components in the collection.</typeparam>
/// <param name="this">The <see cref="IComponentCollection{T}" /> on which this method is called.</param>
/// <param name="component">The component to add.</param>
/// <exception cref="NullReferenceException"><paramref name="this" /> is <see langword="null" />.</exception>
/// <seealso cref="IComponentCollection{T}.Add" />
public static void Add<T>(this IComponentCollection<T> @this, T component) => @this.Add(null, component);
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Swan.Configuration;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Implements a collection of components.</para>
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
/// </summary>
/// <typeparam name="T">The type of components in the collection.</typeparam>
/// <seealso cref="IComponentCollection{T}" />
public class ComponentCollection<T> : ConfiguredObject, IComponentCollection<T>
{
private readonly List<T> _components = new List<T>();
private readonly List<(string, T)> _componentsWithSafeNames = new List<(string, T)>();
private readonly Dictionary<string, T> _namedComponents = new Dictionary<string, T>();
/// <inheritdoc />
public int Count => _components.Count;
/// <inheritdoc />
public IReadOnlyDictionary<string, T> Named => _namedComponents;
/// <inheritdoc />
public IReadOnlyList<(string SafeName, T Component)> WithSafeNames => _componentsWithSafeNames;
/// <inheritdoc />
public T this[int index] => _components[index];
/// <inheritdoc />
public T this[string key] => _namedComponents[key];
/// <inheritdoc />
public IEnumerator<T> GetEnumerator() => _components.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator();
/// <inheritdoc />
/// <exception cref="InvalidOperationException">The collection is locked.</exception>
public void Add(string? name, T component)
{
EnsureConfigurationNotLocked();
if (name != null)
{
if (name.Length == 0)
throw new ArgumentException("Component name is empty.", nameof(name));
if (_namedComponents.ContainsKey(name))
throw new ArgumentException("Duplicate component name.", nameof(name));
}
if (component == null)
throw new ArgumentNullException(nameof(component));
if (_components.Contains(component))
throw new ArgumentException("Component has already been added.", nameof(component));
_components.Add(component);
_componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component));
if (name != null)
_namedComponents.Add(name, component);
}
/// <summary>
/// Locks the collection, preventing further additions.
/// </summary>
public void Lock() => LockConfiguration();
}
}

View File

@@ -0,0 +1,49 @@
using System;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Implements a collection of components that automatically disposes each component
/// implementing <see cref="IDisposable"/>.</para>
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
/// </summary>
/// <typeparam name="T">The type of components in the collection.</typeparam>
/// <seealso cref="ComponentCollection{T}" />
/// <seealso cref="IComponentCollection{T}" />
public class DisposableComponentCollection<T> : ComponentCollection<T>, IDisposable
{
/// <summary>
/// Finalizes an instance of the <see cref="DisposableComponentCollection{T}"/> class.
/// </summary>
~DisposableComponentCollection()
{
Dispose(false);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true"/> to release both managed and unmanaged resources; <see langword="true"/> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
foreach (var component in this)
{
if (component is IDisposable disposable)
disposable.Dispose();
}
}
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Globalization;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides standard methods to parse and format <see cref="DateTime"/>s according to various RFCs.
/// </summary>
public static class HttpDate
{
// https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Net/HttpDateParser.cs
private static readonly string[] DateFormats = {
// "r", // RFC 1123, required output format but too strict for input
"ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time)
"ddd, d MMM yyyy H:m:s 'UTC'", // RFC 1123, UTC
"ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT
"d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week
"d MMM yyyy H:m:s 'UTC'", // RFC 1123, UTC, no day-of-week
"d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone
"ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year
"ddd, d MMM yy H:m:s 'UTC'", // RFC 1123, UTC, short year
"ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone
"d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year
"d MMM yy H:m:s 'UTC'", // RFC 1123, UTC, no day-of-week, short year
"d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone
"dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850
"dddd, d'-'MMM'-'yy H:m:s 'UTC'", // RFC 850, UTC
"dddd, d'-'MMM'-'yy H:m:s zzz", // RFC 850, offset
"dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone
"ddd MMM d H:m:s yyyy", // ANSI C's asctime() format
"ddd, d MMM yyyy H:m:s zzz", // RFC 5322
"ddd, d MMM yyyy H:m:s", // RFC 5322 no zone
"d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week
"d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone
};
/// <summary>
/// Attempts to parse a string containing a date and time, and possibly a time zone offset,
/// in one of the formats specified in <see href="https://tools.ietf.org/html/rfc850">RFC850</see>,
/// <see href="https://tools.ietf.org/html/rfc1123">RFC1123</see>,
/// and <see href="https://tools.ietf.org/html/rfc5322">RFC5322</see>,
/// or ANSI C's <see href="https://linux.die.net/man/3/asctime"><c>asctime()</c></see> format.
/// </summary>
/// <param name="str">The string to parse.</param>
/// <param name="result">When this method returns <see langword="true"/>,
/// a <see cref="DateTimeOffset"/> representing the parsed date, time, and time zone offset.
/// This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if <paramref name="str"/> was successfully parsed;
/// otherwise, <see langword="false"/>.</returns>
public static bool TryParse(string str, out DateTimeOffset result) =>
DateTimeOffset.TryParseExact(
str,
DateFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal,
out result);
/// <summary>
/// Formats the specified <see cref="DateTimeOffset"/>
/// according to <see href="https://tools.ietf.org/html/rfc1123">RFC1123</see>.
/// </summary>
/// <param name="dateTimeOffset">The <see cref="DateTimeOffset"/> to format.</param>
/// <returns>A string containing the formatted <paramref name="dateTimeOffset"/>.</returns>
public static string Format(DateTimeOffset dateTimeOffset)
=> dateTimeOffset.ToUniversalTime().ToString("r", DateTimeFormatInfo.InvariantInfo);
/// <summary>
/// Formats the specified <see cref="DateTime"/>
/// according to <see href="https://tools.ietf.org/html/rfc1123">RFC1123</see>.
/// </summary>
/// <param name="dateTime">The <see cref="DateTime"/> to format.</param>
/// <returns>A string containing the formatted <paramref name="dateTime"/>.</returns>
public static string Format(DateTime dateTime)
=> dateTime.ToUniversalTime().ToString("r", DateTimeFormatInfo.InvariantInfo);
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Represents a collection of components.</para>
/// <para>Each component in the collection may be given a unique name for later retrieval.</para>
/// </summary>
/// <typeparam name="T">The type of components in the collection.</typeparam>
public interface IComponentCollection<T> : IReadOnlyList<T>
{
/// <summary>
/// Gets an <see cref="IReadOnlyDictionary{TKey,TValue}"/> interface representing the named components.
/// </summary>
/// <value>
/// The named components.
/// </value>
IReadOnlyDictionary<string, T> Named { get; }
/// <summary>
/// <para>Gets an <see cref="IReadOnlyList{T}"/> interface representing all components
/// associated with safe names.</para>
/// <para>The safe name of a component is never <see langword="null"/>.
/// If a component's unique name if <see langword="null"/>, its safe name
/// will be some non-<see langword="null"/> string somehow identifying it.</para>
/// <para>Note that safe names are not necessarily unique.</para>
/// </summary>
/// <value>
/// A list of <see cref="ValueTuple{T1,T2}"/>s, each containing a safe name and a component.
/// </value>
IReadOnlyList<(string SafeName, T Component)> WithSafeNames { get; }
/// <summary>
/// Gets the component with the specified name.
/// </summary>
/// <value>
/// The component.
/// </value>
/// <param name="name">The name.</param>
/// <returns>The component with the specified <paramref name="name"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
/// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="name"/> is not found.</exception>
T this[string name] { get; }
/// <summary>
/// Adds a component to the collection,
/// giving it the specified <paramref name="name"/> if it is not <see langword="null"/>.
/// </summary>
/// <param name="name">The name given to the module, or <see langword="null"/>.</param>
/// <param name="component">The component.</param>
void Add(string? name, T component);
}
}

View File

@@ -0,0 +1,209 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Swan.Logging;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides standard methods to parse IP address strings.
/// </summary>
public static class IPParser
{
/// <summary>
/// Parses the specified IP address.
/// </summary>
/// <param name="address">The IP address.</param>
/// <returns>A collection of <see cref="IPAddress"/> parsed correctly from <paramref name="address"/>.</returns>
public static async Task<IEnumerable<IPAddress>> ParseAsync(string address)
{
if (address == null)
return Enumerable.Empty<IPAddress>();
if (IPAddress.TryParse(address, out var ip))
return new List<IPAddress> { ip };
try
{
return await Dns.GetHostAddressesAsync(address).ConfigureAwait(false);
}
catch (SocketException socketEx)
{
socketEx.Log(nameof(IPParser));
}
catch
{
// Ignore
}
if (IsCidrNotation(address))
return ParseCidrNotation(address);
return IsSimpleIPRange(address) ? TryParseSimpleIPRange(address) : Enumerable.Empty<IPAddress>();
}
/// <summary>
/// Determines whether the IP-range string is in CIDR notation.
/// </summary>
/// <param name="range">The IP-range string.</param>
/// <returns>
/// <c>true</c> if the IP-range string is CIDR notation; otherwise, <c>false</c>.
/// </returns>
public static bool IsCidrNotation(string range)
{
if (string.IsNullOrWhiteSpace(range))
return false;
var parts = range.Split('/');
if (parts.Length != 2)
return false;
var prefix = parts[0];
var prefixLen = parts[1];
var prefixParts = prefix.Split('.');
if (prefixParts.Length != 4)
return false;
return byte.TryParse(prefixLen, out var len) && len <= 32;
}
/// <summary>
/// Parse IP-range string in CIDR notation. For example "12.15.0.0/16".
/// </summary>
/// <param name="range">The IP-range string.</param>
/// <returns>A collection of <see cref="IPAddress"/> parsed correctly from <paramref name="range"/>.</returns>
public static IEnumerable<IPAddress> ParseCidrNotation(string range)
{
if (!IsCidrNotation(range))
return Enumerable.Empty<IPAddress>();
var parts = range.Split('/');
var prefix = parts[0];
if (!byte.TryParse(parts[1], out var prefixLen))
return Enumerable.Empty<IPAddress>();
var prefixParts = prefix.Split('.');
if (prefixParts.Select(x => byte.TryParse(x, out _)).Any(x => !x))
return Enumerable.Empty<IPAddress>();
uint ip = 0;
for (var i = 0; i < 4; i++)
{
ip <<= 8;
ip += uint.Parse(prefixParts[i], NumberFormatInfo.InvariantInfo);
}
var shiftBits = (byte)(32 - prefixLen);
var ip1 = (ip >> shiftBits) << shiftBits;
if ((ip1 & ip) != ip1) // Check correct subnet address
return Enumerable.Empty<IPAddress>();
var ip2 = ip1 >> shiftBits;
for (var k = 0; k < shiftBits; k++)
{
ip2 = (ip2 << 1) + 1;
}
var beginIP = new byte[4];
var endIP = new byte[4];
for (var i = 0; i < 4; i++)
{
beginIP[i] = (byte)((ip1 >> ((3 - i) * 8)) & 255);
endIP[i] = (byte)((ip2 >> ((3 - i) * 8)) & 255);
}
return GetAllIPAddresses(beginIP, endIP);
}
/// <summary>
/// Determines whether the IP-range string is in simple IP range notation.
/// </summary>
/// <param name="range">The IP-range string.</param>
/// <returns>
/// <c>true</c> if the IP-range string is in simple IP range notation; otherwise, <c>false</c>.
/// </returns>
public static bool IsSimpleIPRange(string range)
{
if (string.IsNullOrWhiteSpace(range))
return false;
var parts = range.Split('.');
if (parts.Length != 4)
return false;
foreach (var part in parts)
{
var rangeParts = part.Split('-');
if (rangeParts.Length < 1 || rangeParts.Length > 2)
return false;
if (!byte.TryParse(rangeParts[0], out _) ||
(rangeParts.Length > 1 && !byte.TryParse(rangeParts[1], out _)))
return false;
}
return true;
}
/// <summary>
/// Tries to parse IP-range string "12.15-16.1-30.10-255"
/// </summary>
/// <param name="range">The IP-range string.</param>
/// <returns>A collection of <see cref="IPAddress"/> parsed correctly from <paramref name="range"/>.</returns>
public static IEnumerable<IPAddress> TryParseSimpleIPRange(string range)
{
if (!IsSimpleIPRange(range))
return Enumerable.Empty<IPAddress>();
var beginIP = new byte[4];
var endIP = new byte[4];
var parts = range.Split('.');
for (var i = 0; i < 4; i++)
{
var rangeParts = parts[i].Split('-');
beginIP[i] = byte.Parse(rangeParts[0], NumberFormatInfo.InvariantInfo);
endIP[i] = (rangeParts.Length == 1) ? beginIP[i] : byte.Parse(rangeParts[1], NumberFormatInfo.InvariantInfo);
}
return GetAllIPAddresses(beginIP, endIP);
}
private static IEnumerable<IPAddress> GetAllIPAddresses(byte[] beginIP, byte[] endIP)
{
for (var i = 0; i < 4; i++)
{
if (endIP[i] < beginIP[i])
return Enumerable.Empty<IPAddress>();
}
var capacity = 1;
for (var i = 0; i < 4; i++)
capacity *= endIP[i] - beginIP[i] + 1;
var ips = new List<IPAddress>(capacity);
for (int i0 = beginIP[0]; i0 <= endIP[0]; i0++)
{
for (int i1 = beginIP[1]; i1 <= endIP[1]; i1++)
{
for (int i2 = beginIP[2]; i2 <= endIP[2]; i2++)
{
for (int i3 = beginIP[3]; i3 <= endIP[3]; i3++)
{
ips.Add(new IPAddress(new[] { (byte)i0, (byte)i1, (byte)i2, (byte)i3 }));
}
}
}
}
return ips;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Manages a stack of MIME type providers.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <seealso cref="IMimeTypeProvider" />
public sealed class MimeTypeProviderStack : IMimeTypeProvider
{
private readonly Stack<IMimeTypeProvider> _providers = new Stack<IMimeTypeProvider>();
/// <summary>
/// <para>Pushes the specified MIME type provider on the stack.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
/// <param name="provider">The <see cref="IMimeTypeProvider"/> interface to push on the stack.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/>is <see langword="null"/>.</exception>
public void Push(IMimeTypeProvider provider)
=> _providers.Push(Validate.NotNull(nameof(provider), provider));
/// <summary>
/// <para>Removes the most recently added MIME type provider from the stack.</para>
/// <para>This API supports the EmbedIO infrastructure and is not intended to be used directly from your code.</para>
/// </summary>
public void Pop() => _providers.Pop();
/// <inheritdoc />
public string GetMimeType(string extension)
{
var result = _providers.Select(p => p.GetMimeType(extension))
.FirstOrDefault(m => m != null);
if (result == null)
_ = MimeType.Associations.TryGetValue(extension, out result);
return result;
}
/// <inheritdoc />
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
{
foreach (var provider in _providers)
{
if (provider.TryDetermineCompression(mimeType, out preferCompression))
return true;
}
preferCompression = default;
return false;
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides extension methods for <see cref="NameValueCollection"/>.
/// </summary>
public static class NameValueCollectionExtensions
{
/// <summary>
/// <para>Converts a <see cref="NameValueCollection"/> to a dictionary of objects.</para>
/// <para>Values in the returned dictionary will wither be strings, or arrays of strings,
/// depending on the presence of multiple values for the same key in the collection.</para>
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <returns>A <see cref="Dictionary{TKey,TValue}"/> associating the collection's keys
/// with their values.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static Dictionary<string, object?> ToDictionary(this NameValueCollection @this)
=> @this.Keys.Cast<string>().ToDictionary(key => key, key => {
var values = @this.GetValues(key);
if (values == null)
return null;
return values.Length switch {
0 => null,
1 => (object) values[0],
_ => (object) values
};
});
/// <summary>
/// Converts a <see cref="NameValueCollection"/> to a dictionary of strings.
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <returns>A <see cref="Dictionary{TKey,TValue}"/> associating the collection's keys
/// with their values (or comma-separated lists in case of multiple values).</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static Dictionary<string, string> ToStringDictionary(this NameValueCollection @this)
=> @this.Keys.Cast<string>().ToDictionary(key => key, @this.Get);
/// <summary>
/// Converts a <see cref="NameValueCollection"/> to a dictionary of arrays of strings.
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <returns>A <see cref="Dictionary{TKey,TValue}"/> associating the collection's keys
/// with arrays of their values.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static Dictionary<string, string[]> ToArrayDictionary(this NameValueCollection @this)
=> @this.Keys.Cast<string>().ToDictionary(key => key, @this.GetValues);
/// <summary>
/// Determines whether a <see cref="NameValueCollection"/> contains one or more values
/// for the specified <paramref name="key"/>.
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <param name="key">The key to look for.</param>
/// <returns><see langword="true"/> if at least one value for <paramref name="key"/>
/// is present in the collection; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static bool ContainsKey(this NameValueCollection @this, string key)
=> @this.Keys.Cast<string>().Contains(key);
/// <summary>
/// Determines whether a <see cref="NameValueCollection"/> contains one or more values
/// for the specified <paramref name="name"/>, at least one of which is equal to the specified
/// <paramref name="value"/>. Value comparisons are carried out using the
/// <see cref="StringComparison.OrdinalIgnoreCase"/> comparison type.
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <param name="name">The name to look for.</param>
/// <param name="value">The value to look for.</param>
/// <returns><see langword="true"/> if at least one of the values for <paramref name="name"/>
/// in the collection is equal to <paramref name="value"/>; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>White space is trimmed from the start and end of each value before comparison.</remarks>
/// <seealso cref="Contains(NameValueCollection,string,string,StringComparison)"/>
public static bool Contains(this NameValueCollection @this, string name, string value)
=> Contains(@this, name, value, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Determines whether a <see cref="NameValueCollection"/> contains one or more values
/// for the specified <paramref name="name"/>, at least one of which is equal to the specified
/// <paramref name="value"/>. Value comparisons are carried out using the specified
/// <paramref name="comparisonType"/>.
/// </summary>
/// <param name="this">The <see cref="NameValueCollection"/> on which this method is called.</param>
/// <param name="name">The name to look for.</param>
/// <param name="value">The value to look for.</param>
/// <param name="comparisonType">One of the <see cref="StringComparison"/> enumeration values
/// that specifies how the strings will be compared.</param>
/// <returns><see langword="true"/> if at least one of the values for <paramref name="name"/>
/// in the collection is equal to <paramref name="value"/>; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <remarks>White space is trimmed from the start and end of each value before comparison.</remarks>
/// <seealso cref="Contains(NameValueCollection,string,string)"/>
public static bool Contains(this NameValueCollection @this, string name, string? value, StringComparison comparisonType)
{
value = value?.Trim();
return @this[name]?.SplitByComma()
.Any(val => string.Equals(val?.Trim(), value, comparisonType)) ?? false;
}
}
}

View File

@@ -0,0 +1,279 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Represents a list of names with associated quality values extracted from an HTTP header,
/// e.g. <c>gzip; q=0.9, deflate</c>.</para>
/// <para>See <see href="https://tools.ietf.org/html/rfc7231#section-5.3">RFC7231, section 5.3</see>.</para>
/// <para>This class ignores and discards extensions (<c>accept-ext</c> in RFC7231 terminology).</para>
/// <para>If a name has one or more parameters (e.g. <c>text/html;level=1</c>) it is not
/// further parsed: parameters will appear as part of the name.</para>
/// </summary>
public sealed class QValueList
{
/// <summary>
/// <para>A value signifying "anything will do" in request headers.</para>
/// <para>For example, a request header of
/// <c>Accept-Encoding: *;q=0.8, gzip</c> means "I prefer GZip compression;
/// if it is not available, any other compression (including no compression at all)
/// is OK for me".</para>
/// </summary>
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);
/// <summary>
/// Initializes a new instance of the <see cref="QValueList"/> class
/// by parsing comma-separated request header values.
/// </summary>
/// <param name="useWildcard">If set to <see langword="true"/>, a value of <c>*</c>
/// will be treated as signifying "anything".</param>
/// <param name="headerValues">A list of comma-separated header values.</param>
/// <seealso cref="UseWildcard"/>
public QValueList(bool useWildcard, string headerValues)
{
UseWildcard = useWildcard;
QValues = Parse(headerValues);
}
/// <summary>
/// Initializes a new instance of the <see cref="QValueList"/> class
/// by parsing comma-separated request header values.
/// </summary>
/// <param name="useWildcard">If set to <see langword="true"/>, a value of <c>*</c>
/// will be treated as signifying "anything".</param>
/// <param name="headerValues">An enumeration of header values.
/// Note that each element of the enumeration may in turn be
/// a comma-separated list.</param>
/// <seealso cref="UseWildcard"/>
public QValueList(bool useWildcard, IEnumerable<string> headerValues)
{
UseWildcard = useWildcard;
QValues = Parse(headerValues);
}
/// <summary>
/// Initializes a new instance of the <see cref="QValueList"/> class
/// by parsing comma-separated request header values.
/// </summary>
/// <param name="useWildcard">If set to <see langword="true"/>, a value of <c>*</c>
/// will be treated as signifying "anything".</param>
/// <param name="headerValues">An array of header values.
/// Note that each element of the array may in turn be
/// a comma-separated list.</param>
/// <seealso cref="UseWildcard"/>
public QValueList(bool useWildcard, params string[] headerValues)
: this(useWildcard, headerValues as IEnumerable<string>)
{
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>This property does not usually need to be used directly;
/// use the <see cref="IsCandidate"/>, <see cref="FindPreferred"/>,
/// <see cref="FindPreferredIndex(IEnumerable{string})"/>, and
/// <see cref="FindPreferredIndex(string[])"/> methods instead.</para>
/// </remarks>
/// <seealso cref="IsCandidate"/>
/// <seealso cref="FindPreferred"/>
/// <seealso cref="FindPreferredIndex(IEnumerable{string})"/>
/// <seealso cref="FindPreferredIndex(string[])"/>
public IReadOnlyDictionary<string, (int Weight, int Ordinal)> QValues { get; }
/// <summary>
/// Gets a value indicating whether <c>*</c> is treated as a special value
/// with the meaning of "anything".
/// </summary>
public bool UseWildcard { get; }
/// <summary>
/// Determines whether the specified value is a possible candidate.
/// </summary>
/// <param name="value">The value.</param>
/// <returns><see langword="true"/>if <paramref name="value"/> is a candidate;
/// otherwise, <see langword="false"/>.</returns>
public bool IsCandidate(string value)
=> TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate) && candidate.Weight > 0;
/// <summary>
/// Attempts to determine whether the weight of a possible candidate.
/// </summary>
/// <param name="value">The value whose weight is to be determined.</param>
/// <param name="weight">When this method returns <see langword="true"/>,
/// the weight of the candidate.</param>
/// <returns><see langword="true"/> if <paramref name="value"/> is a candidate;
/// otherwise, <see langword="false"/>.</returns>
public bool TryGetWeight(string value, out int weight)
{
var result = TryGetCandidateValue(Validate.NotNull(nameof(value), value), out var candidate);
weight = candidate.Weight;
return result;
}
/// <summary>
/// Finds the value preferred by the client among an enumeration of values.
/// </summary>
/// <param name="values">The values.</param>
/// <returns>The value preferred by the client, or <see langword="null"/>
/// if none of the provided <paramref name="values"/> is accepted.</returns>
public string? FindPreferred(IEnumerable<string> values)
=> FindPreferredCore(values, out var result) >= 0 ? result : null;
/// <summary>
/// Finds the index of the value preferred by the client in a list of values.
/// </summary>
/// <param name="values">The values.</param>
/// <returns>The index of the value preferred by the client, or -1
/// if none of the values in <paramref name="values"/> is accepted.</returns>
public int FindPreferredIndex(IEnumerable<string> values) => FindPreferredCore(values, out _);
/// <summary>
/// Finds the index of the value preferred by the client in an array of values.
/// </summary>
/// <param name="values">The values.</param>
/// <returns>The index of the value preferred by the client, or -1
/// if none of the values in <paramref name="values"/> is accepted.</returns>
public int FindPreferredIndex(params string[] values) => FindPreferredIndex(values as IReadOnlyList<string>);
private static IReadOnlyDictionary<string, (int Weight, int Ordinal)> Parse(string headerValues)
{
var result = new Dictionary<string, (int Weight, int Ordinal)>();
ParseCore(headerValues, result);
return result;
}
private static IReadOnlyDictionary<string, (int Weight, int Ordinal)> Parse(IEnumerable<string> headerValues)
{
var result = new Dictionary<string, (int Weight, int Ordinal)>();
if (headerValues == null) return result;
foreach (var headerValue in headerValues)
ParseCore(headerValue, result);
return result;
}
private static void ParseCore(string text, IDictionary<string, (int Weight, int Ordinal)> 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<string> 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));
}
}

View File

@@ -0,0 +1,72 @@
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides extension methods for <see cref="QValueList"/>.
/// </summary>
public static class QValueListExtensions
{
/// <summary>
/// <para>Attempts to proactively negotiate a compression method for a response,
/// based on the contents of a <see cref="QValueList"/>.</para>
/// </summary>
/// <param name="this">The <see cref="QValueList"/> on which this method is called.</param>
/// <param name="preferCompression"><see langword="true"/> if sending compressed data is preferred over
/// sending non-compressed data; otherwise, <see langword="false"/>.</param>
/// <param name="compressionMethod">When this method returns, the compression method to use for the response,
/// if content negotiation is successful. This parameter is passed uninitialized.</param>
/// <param name="compressionMethodName">When this method returns, the name of the compression method,
/// if content negotiation is successful. This parameter is passed uninitialized.</param>
/// <returns><see langword="true"/> if content negotiation is successful;
/// otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// <para>If <paramref name="this"/> is empty, this method always returns <see langword="true"/>,
/// setting <paramref name="compressionMethod"/> to <see cref="CompressionMethod.None"/>
/// and <paramref name="compressionMethodName"/> to <see cref="CompressionMethodNames.None"/>.</para>
/// </remarks>
public static bool TryNegotiateContentEncoding(
this QValueList @this,
bool preferCompression,
out CompressionMethod compressionMethod,
out string? compressionMethodName)
{
if (@this.QValues.Count < 1)
{
compressionMethod = CompressionMethod.None;
compressionMethodName = CompressionMethodNames.None;
return true;
}
// https://tools.ietf.org/html/rfc7231#section-5.3.4
// RFC7231, Section 5.3.4, rule #2:
// If the representation has no content-coding, then it is
// acceptable by default unless specifically excluded by the
// Accept - Encoding field stating either "identity;q=0" or "*;q=0"
// without a more specific entry for "identity".
if (!preferCompression && (!@this.TryGetWeight(CompressionMethodNames.None, out var weight) || weight > 0))
{
compressionMethod = CompressionMethod.None;
compressionMethodName = CompressionMethodNames.None;
return true;
}
var acceptableMethods = preferCompression
? new[] { CompressionMethod.Gzip, CompressionMethod.Deflate, CompressionMethod.None }
: new[] { CompressionMethod.None, CompressionMethod.Gzip, CompressionMethod.Deflate };
var acceptableMethodNames = preferCompression
? new[] { CompressionMethodNames.Gzip, CompressionMethodNames.Deflate, CompressionMethodNames.None }
: new[] { CompressionMethodNames.None, CompressionMethodNames.Gzip, CompressionMethodNames.Deflate };
var acceptableMethodIndex = @this.FindPreferredIndex(acceptableMethodNames);
if (acceptableMethodIndex < 0)
{
compressionMethod = default;
compressionMethodName = default;
return false;
}
compressionMethod = acceptableMethods[acceptableMethodIndex];
compressionMethodName = acceptableMethodNames[acceptableMethodIndex];
return true;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides extension methods for <see cref="string"/>.
/// </summary>
public static class StringExtensions
{
private static readonly char[] CommaSplitChars = {','};
/// <summary>Splits a string into substrings based on the specified <paramref name="delimiters"/>.
/// The returned array includes empty array elements if two or more consecutive delimiters are found
/// in <paramref name="this"/>.</summary>
/// <param name="this">The <see cref="string"/> on which this method is called.</param>
/// <param name="delimiters">An array of <see cref="char"/>s to use as delimiters.</param>
/// <returns>An array whose elements contain the substrings in <paramref name="this"/> that are delimited
/// by one or more characters in <paramref name="delimiters"/>.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
public static string[] SplitByAny(this string @this, params char[] delimiters) => @this.Split(delimiters);
/// <summary>Splits a string into substrings, using the comma (<c>,</c>) character as a delimiter.
/// The returned array includes empty array elements if two or more commas are found in <paramref name="this"/>.</summary>
/// <param name="this">The <see cref="string"/> on which this method is called.</param>
/// <returns>An array whose elements contain the substrings in <paramref name="this"/> that are delimited by commas.</returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <seealso cref="SplitByComma(string,StringSplitOptions)"/>
public static string[] SplitByComma(this string @this) => @this.Split(CommaSplitChars);
/// <summary>Splits a string into substrings, using the comma (<c>,</c>) character as a delimiter.
/// You can specify whether the substrings include empty array elements.</summary>
/// <param name="this">The <see cref="string"/> on which this method is called.</param>
/// <param name="options"><see cref="StringSplitOptions.RemoveEmptyEntries"/> to omit empty array elements from the array returned;
/// or <see cref="StringSplitOptions.None"/> to include empty array elements in the array returned.</param>
/// <returns>
/// <para>An array whose elements contain the substrings in <paramref name="this"/> that are delimited by commas.</para>
/// <para>For more information, see the Remarks section of the <see cref="string.Split(char[],StringSplitOptions)"/> method.</para>
/// </returns>
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="options">options</paramref> is not one of the <see cref="StringSplitOptions"/> values.</exception>
/// <seealso cref="SplitByComma(string)"/>
public static string[] SplitByComma(this string @this, StringSplitOptions options) =>
@this.Split(CommaSplitChars, options);
/// <summary>
/// Ensures that a <see cref="string"/> is never empty,
/// by transforming empty strings into <see langword="null"/>.
/// </summary>
/// <param name="this">The <see cref="string"/> on which this method is called.</param>
/// <returns>If <paramref name="this"/> is the empty string, <see langword="null"/>;
/// otherwise, <paramref name="this."/></returns>
public static string? NullIfEmpty(this string @this)
=> string.IsNullOrEmpty(@this) ? null : @this;
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace EmbedIO.Utilities
{
/// <summary>
/// <para>Generates locally unique string IDs, mainly for logging purposes.</para>
/// </summary>
public static class UniqueIdGenerator
{
/// <summary>
/// Generates and returns a unique ID.
/// </summary>
/// <returns>The generated ID.</returns>
public static string GetNext() => Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Substring(0, 22);
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Specialized;
using System.Net;
using EmbedIO.Internal;
namespace EmbedIO.Utilities
{
/// <summary>
/// Parses URL queries or URL-encoded HTML forms.
/// </summary>
public static class UrlEncodedDataParser
{
/// <summary>
/// <para>Parses a URL query or URL-encoded HTML form.</para>
/// <para>Unlike <see cref="HttpListenerRequest.QueryString" />, the returned
/// <see cref="NameValueCollection" /> will have bracketed indexes stripped away;
/// for example, <c>a[0]=1&amp;a[1]=2</c> will yield the same result as <c>a=1&amp;a=2</c>,
/// i.e. a <see cref="NameValueCollection" /> with one key (<c>a</c>) associated with
/// two values (<c>1</c> and <c>2</c>).</para>
/// </summary>
/// <param name="source">The string to parse.</param>
/// <param name="groupFlags"><para>If this parameter is <see langword="true" />,
/// tokens not followed by an equal sign (e.g. <c>this</c> in <c>a=1&amp;this&amp;b=2</c>)
/// will be grouped as values of a <c>null</c> key.
/// This is the same behavior as the <see cref="IHttpRequest.QueryString" /> and
/// <see cref="HttpListenerRequest.QueryString" /> properties.</para>
/// <para>If this parameter is <see langword="false" />, tokens not followed by an equal sign
/// (e.g. <c>this</c> in <c>a=1&amp;this&amp;b=2</c>) will be considered keys with an empty
/// value. This is the same behavior as the <see cref="HttpContextExtensions.GetRequestQueryData" />
/// extension method.</para></param>
/// <param name="mutableResult"><see langword="true" /> (the default) to return
/// a mutable (non-read-only) collection; <see langword="false" /> to return a read-only collection.</param>
/// <returns>A <see cref="NameValueCollection" /> containing the parsed data.</returns>
public static NameValueCollection Parse(string source, bool groupFlags, bool mutableResult = true)
{
var result = new LockableNameValueCollection();
// Verify there is data to parse; otherwise, return an empty collection.
if (string.IsNullOrEmpty(source))
{
if (!mutableResult)
result.MakeReadOnly();
return result;
}
void AddKeyValuePair(string? key, string value)
{
if (key != null)
{
// Decode the key.
key = WebUtility.UrlDecode(key);
// Discard bracketed index (used e.g. by PHP)
var bracketPos = key.IndexOf("[", StringComparison.Ordinal);
if (bracketPos > 0)
key = key.Substring(0, bracketPos);
}
// Decode the value.
value = WebUtility.UrlDecode(value);
// Add the KVP to the collection.
result.Add(key, value);
}
// Skip the initial question mark,
// in case source is the Query property of a Uri.
var kvpPos = source[0] == '?' ? 1 : 0;
var length = source.Length;
while (kvpPos < length)
{
var separatorPos = kvpPos;
var equalPos = -1;
while (separatorPos < length)
{
var c = source[separatorPos];
if (c == '&')
break;
if (c == '=' && equalPos < 0)
equalPos = separatorPos;
separatorPos++;
}
// Split by the equals char into key and value.
// Some KVPS will have only their key, some will have both key and value
// Some other might be repeated which really means an array
if (equalPos < 0)
{
if (groupFlags)
{
AddKeyValuePair(null, source.Substring(kvpPos, separatorPos - kvpPos));
}
else
{
AddKeyValuePair(source.Substring(kvpPos, separatorPos - kvpPos), string.Empty);
}
}
else
{
AddKeyValuePair(
source.Substring(kvpPos, equalPos - kvpPos),
source.Substring(equalPos + 1, separatorPos - equalPos - 1));
}
// Edge case: if the last character in source is '&',
// there's an empty KVP that we would otherwise skip.
if (separatorPos == length - 1)
{
AddKeyValuePair(groupFlags ? null : string.Empty, string.Empty);
break;
}
// On to next KVP
kvpPos = separatorPos + 1;
}
if (!mutableResult)
result.MakeReadOnly();
return result;
}
}
}

View File

@@ -0,0 +1,315 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides utility methods to work with URL paths.
/// </summary>
public static class UrlPath
{
/// <summary>
/// The root URL path value, i.e. <c>"/"</c>.
/// </summary>
public const string Root = "/";
private static readonly Regex MultipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary>
/// Determines whether a string is a valid URL path.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <returns>
/// <see langword="true"/> if the specified URL path is valid; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// <para>For a string to be a valid URL path, it must not be <see langword="null"/>,
/// must not be empty, and must start with a slash (<c>/</c>) character.</para>
/// <para>To ensure that a method parameter is a valid URL path, use <see cref="Validate.UrlPath"/>.</para>
/// </remarks>
/// <seealso cref="Normalize"/>
/// <seealso cref="UnsafeNormalize"/>
/// <seealso cref="Validate.UrlPath"/>
public static bool IsValid(string urlPath) => ValidateInternal(nameof(urlPath), urlPath) == null;
/// <summary>
/// Normalizes the specified URL path.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="isBasePath">if set to <see langword="true"/>, treat the URL path
/// as a base path, i.e. ensure it ends with a slash (<c>/</c>) character;
/// otherwise, ensure that it does NOT end with a slash character.</param>
/// <returns>The normalized path.</returns>
/// <exception cref="ArgumentException">
/// <paramref name="urlPath"/> is not a valid URL path.
/// </exception>
/// <remarks>
/// <para>A normalized URL path is one where each run of two or more slash
/// (<c>/</c>) characters has been replaced with a single slash character.</para>
/// <para>This method does NOT try to decode URL-encoded characters.</para>
/// <para>If you are sure that <paramref name="urlPath"/> is a valid URL path,
/// for example because you have called <see cref="IsValid"/> and it returned
/// <see langword="true"/>, then you may call <see cref="UnsafeNormalize"/>
/// instead of this method. <see cref="UnsafeNormalize"/> is slightly faster because
/// it skips the initial validity check.</para>
/// <para>There is no need to call this method for a method parameter
/// for which you have already called <see cref="Validate.UrlPath"/>.</para>
/// </remarks>
/// <seealso cref="UnsafeNormalize"/>
/// <seealso cref="IsValid"/>
/// <seealso cref="Validate.UrlPath"/>
public static string Normalize(string urlPath, bool isBasePath)
{
var exception = ValidateInternal(nameof(urlPath), urlPath);
if (exception != null)
throw exception;
return UnsafeNormalize(urlPath, isBasePath);
}
/// <summary>
/// Normalizes the specified URL path, assuming that it is valid.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="isBasePath">if set to <see langword="true"/>, treat the URL path
/// as a base path, i.e. ensure it ends with a slash (<c>/</c>) character;
/// otherwise, ensure that it does NOT end with a slash character.</param>
/// <returns>The normalized path.</returns>
/// <remarks>
/// <para>A normalized URL path is one where each run of two or more slash
/// (<c>/</c>) characters has been replaced with a single slash character.</para>
/// <para>This method does NOT try to decode URL-encoded characters.</para>
/// <para>If <paramref name="urlPath"/> is not valid, the behavior of
/// this method is unspecified. You should call this method only after
/// <see cref="IsValid"/> has returned <see langword="true"/>
/// for the same <paramref name="urlPath"/>.</para>
/// <para>You should call <see cref="Normalize"/> instead of this method
/// if you are not sure that <paramref name="urlPath"/> is valid.</para>
/// <para>There is no need to call this method for a method parameter
/// for which you have already called <see cref="Validate.UrlPath"/>.</para>
/// </remarks>
/// <seealso cref="Normalize"/>
/// <seealso cref="IsValid"/>
/// <seealso cref="Validate.UrlPath"/>
public static string UnsafeNormalize(string urlPath, bool isBasePath)
{
// Replace each run of multiple slashes with a single slash
urlPath = MultipleSlashRegex.Replace(urlPath, "/");
// The root path needs no further checking.
var length = urlPath.Length;
if (length == 1)
return urlPath;
// Base URL paths must end with a slash;
// non-base URL paths must NOT end with a slash.
// The final slash is irrelevant for the URL itself
// (it has to map the same way with or without it)
// but makes comparing and mapping URLs a lot simpler.
var finalPosition = length - 1;
var endsWithSlash = urlPath[finalPosition] == '/';
return isBasePath
? (endsWithSlash ? urlPath : urlPath + "/")
: (endsWithSlash ? urlPath.Substring(0, finalPosition) : urlPath);
}
/// <summary>
/// Determines whether the specified URL path is prefixed by the specified base URL path.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="baseUrlPath">The base URL path.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="urlPath"/> is prefixed by <paramref name="baseUrlPath"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentException">
/// <para><paramref name="urlPath"/> is not a valid URL path.</para>
/// <para>- or -</para>
/// <para><paramref name="baseUrlPath"/> is not a valid base URL path.</para>
/// </exception>
/// <remarks>
/// <para>This method returns <see langword="true"/> even if the two URL paths are equivalent,
/// for example if both are <c>"/"</c>, or if <paramref name="urlPath"/> is <c>"/download"</c> and
/// <paramref name="baseUrlPath"/> is <c>"/download/"</c>.</para>
/// <para>If you are sure that both <paramref name="urlPath"/> and <paramref name="baseUrlPath"/>
/// are valid and normalized, for example because you have called <see cref="Validate.UrlPath"/>,
/// then you may call <see cref="UnsafeHasPrefix"/> instead of this method. <see cref="UnsafeHasPrefix"/>
/// is slightly faster because it skips validity checks.</para>
/// </remarks>
/// <seealso cref="UnsafeHasPrefix"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="StripPrefix"/>
/// <seealso cref="Validate.UrlPath"/>
public static bool HasPrefix(string urlPath, string baseUrlPath)
=> UnsafeHasPrefix(
Validate.UrlPath(nameof(urlPath), urlPath, false),
Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true));
/// <summary>
/// Determines whether the specified URL path is prefixed by the specified base URL path,
/// assuming both paths are valid and normalized.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="baseUrlPath">The base URL path.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="urlPath"/> is prefixed by <paramref name="baseUrlPath"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// <para>Unless both <paramref name="urlPath"/> and <paramref name="baseUrlPath"/> are valid,
/// normalized URL paths, the behavior of this method is unspecified. You should call this method
/// only after calling either <see cref="Normalize"/> or <see cref="Validate.UrlPath"/>
/// to check and normalize both parameters.</para>
/// <para>If you are not sure about the validity and/or normalization of parameters,
/// call <see cref="HasPrefix"/> instead of this method.</para>
/// <para>This method returns <see langword="true"/> even if the two URL paths are equivalent,
/// for example if both are <c>"/"</c>, or if <paramref name="urlPath"/> is <c>"/download"</c> and
/// <paramref name="baseUrlPath"/> is <c>"/download/"</c>.</para>
/// </remarks>
/// <seealso cref="HasPrefix"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="StripPrefix"/>
/// <seealso cref="Validate.UrlPath"/>
public static bool UnsafeHasPrefix(string urlPath, string baseUrlPath)
=> urlPath.StartsWith(baseUrlPath, StringComparison.Ordinal)
|| (urlPath.Length == baseUrlPath.Length - 1 && baseUrlPath.StartsWith(urlPath, StringComparison.Ordinal));
/// <summary>
/// Strips a base URL path fom a URL path, obtaining a relative path.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="baseUrlPath">The base URL path.</param>
/// <returns>The relative path, or <see langword="null"/> if <paramref name="urlPath"/>
/// is not prefixed by <paramref name="baseUrlPath"/>.</returns>
/// <exception cref="ArgumentException">
/// <para><paramref name="urlPath"/> is not a valid URL path.</para>
/// <para>- or -</para>
/// <para><paramref name="baseUrlPath"/> is not a valid base URL path.</para>
/// </exception>
/// <remarks>
/// <para>The returned relative path is NOT prefixed by a slash (<c>/</c>) character.</para>
/// <para>If <paramref name="urlPath"/> and <paramref name="baseUrlPath"/> are equivalent,
/// for example if both are <c>"/"</c>, or if <paramref name="urlPath"/> is <c>"/download"</c>
/// and <paramref name="baseUrlPath"/> is <c>"/download/"</c>, this method returns an empty string.</para>
/// <para>If you are sure that both <paramref name="urlPath"/> and <paramref name="baseUrlPath"/>
/// are valid and normalized, for example because you have called <see cref="Validate.UrlPath"/>,
/// then you may call <see cref="UnsafeStripPrefix"/> instead of this method. <see cref="UnsafeStripPrefix"/>
/// is slightly faster because it skips validity checks.</para>
/// </remarks>
/// <seealso cref="UnsafeStripPrefix"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="HasPrefix"/>
/// <seealso cref="Validate.UrlPath"/>
public static string? StripPrefix(string urlPath, string baseUrlPath)
=> UnsafeStripPrefix(
Validate.UrlPath(nameof(urlPath), urlPath, false),
Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true));
/// <summary>
/// Strips a base URL path fom a URL path, obtaining a relative path,
/// assuming both paths are valid and normalized.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <param name="baseUrlPath">The base URL path.</param>
/// <returns>The relative path, or <see langword="null"/> if <paramref name="urlPath"/>
/// is not prefixed by <paramref name="baseUrlPath"/>.</returns>
/// <remarks>
/// <para>Unless both <paramref name="urlPath"/> and <paramref name="baseUrlPath"/> are valid,
/// normalized URL paths, the behavior of this method is unspecified. You should call this method
/// only after calling either <see cref="Normalize"/> or <see cref="Validate.UrlPath"/>
/// to check and normalize both parameters.</para>
/// <para>If you are not sure about the validity and/or normalization of parameters,
/// call <see cref="StripPrefix"/> instead of this method.</para>
/// <para>The returned relative path is NOT prefixed by a slash (<c>/</c>) character.</para>
/// <para>If <paramref name="urlPath"/> and <paramref name="baseUrlPath"/> are equivalent,
/// for example if both are <c>"/"</c>, or if <paramref name="urlPath"/> is <c>"/download"</c>
/// and <paramref name="baseUrlPath"/> is <c>"/download/"</c>, this method returns an empty string.</para>
/// </remarks>
/// <seealso cref="StripPrefix"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="HasPrefix"/>
/// <seealso cref="Validate.UrlPath"/>
public static string? UnsafeStripPrefix(string urlPath, string baseUrlPath)
{
if (!UnsafeHasPrefix(urlPath, baseUrlPath))
return null;
// The only case where UnsafeHasPrefix returns true for a urlPath shorter than baseUrlPath
// is urlPath == (baseUrlPath minus the final slash).
return urlPath.Length < baseUrlPath.Length
? string.Empty
: urlPath.Substring(baseUrlPath.Length);
}
/// <summary>
/// Splits the specified URL path into segments.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <returns>An enumeration of path segments.</returns>
/// <exception cref="ArgumentException"><paramref name="urlPath"/> is not a valid URL path.</exception>
/// <remarks>
/// <para>A root URL path (<c>/</c>) will result in an empty enumeration.</para>
/// <para>The returned enumeration will be the same whether <paramref name="urlPath"/> is a base URL path or not.</para>
/// <para>If you are sure that <paramref name="urlPath"/> is valid and normalized,
/// for example because you have called <see cref="Validate.UrlPath"/>,
/// then you may call <see cref="UnsafeSplit"/> instead of this method. <see cref="UnsafeSplit"/>
/// is slightly faster because it skips validity checks.</para>
/// </remarks>
/// <seealso cref="UnsafeSplit"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="Validate.UrlPath"/>
public static IEnumerable<string> Split(string urlPath)
=> UnsafeSplit(Validate.UrlPath(nameof(urlPath), urlPath, false));
/// <summary>
/// Splits the specified URL path into segments, assuming it is valid and normalized.
/// </summary>
/// <param name="urlPath">The URL path.</param>
/// <returns>An enumeration of path segments.</returns>
/// <remarks>
/// <para>Unless <paramref name="urlPath"/> is a valid, normalized URL path,
/// the behavior of this method is unspecified. You should call this method
/// only after calling either <see cref="Normalize"/> or <see cref="Validate.UrlPath"/>
/// to check and normalize both parameters.</para>
/// <para>If you are not sure about the validity and/or normalization of <paramref name="urlPath"/>,
/// call <see cref="StripPrefix"/> instead of this method.</para>
/// <para>A root URL path (<c>/</c>) will result in an empty enumeration.</para>
/// <para>The returned enumeration will be the same whether <paramref name="urlPath"/> is a base URL path or not.</para>
/// </remarks>
/// <seealso cref="Split"/>
/// <seealso cref="Normalize"/>
/// <seealso cref="Validate.UrlPath"/>
public static IEnumerable<string> UnsafeSplit(string urlPath)
{
var length = urlPath.Length;
var position = 1; // Skip initial slash
while (position < length)
{
var slashPosition = urlPath.IndexOf('/', position);
if (slashPosition < 0)
{
yield return urlPath.Substring(position);
break;
}
yield return urlPath.Substring(position, slashPosition - position);
position = slashPosition + 1;
}
}
internal static Exception? ValidateInternal(string argumentName, string value)
{
if (value == null)
return new ArgumentNullException(argumentName);
if (value.Length == 0)
return new ArgumentException("URL path is empty.", argumentName);
if (value[0] != '/')
return new ArgumentException("URL path does not start with a slash.", argumentName);
return null;
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
namespace EmbedIO.Utilities
{
partial class Validate
{
/// <summary>
/// <para>Ensures that a <see langword="string"/> argument is valid as MIME type or media range as defined by
/// <see href="https://tools.ietf.org/html/rfc7231#section-5.3.2">RFC7231, Section 5,3.2</see>.</para>
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <param name="acceptMediaRange">If <see langword="true"/>, media ranges (i.e. strings of the form <c>*/*</c>
/// and <c>type/*</c>) are considered valid; otherwise, they are rejected as invalid.</param>
/// <returns><paramref name="value"/>, if it is a valid MIME type or media range.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is the empty string.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> is not a valid MIME type or media range.</para>
/// </exception>
public static string MimeType(string argumentName, string value, bool acceptMediaRange)
{
value = NotNullOrEmpty(argumentName, value);
if (!EmbedIO.MimeType.IsMimeType(value, acceptMediaRange))
throw new ArgumentException("MIME type is not valid.", argumentName);
return value;
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Security;
namespace EmbedIO.Utilities
{
partial class Validate
{
private static readonly char[] InvalidLocalPathChars = GetInvalidLocalPathChars();
/// <summary>
/// Ensures that the value of an argument is a valid URL path
/// and normalizes it.
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <param name="isBasePath">If set to <see langword="true"/><c>true</c>, the returned path
/// is ensured to end in a slash (<c>/</c>) character; otherwise, the returned path is
/// ensured to not end in a slash character unless it is <c>"/"</c>.</param>
/// <returns>The normalized URL path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is empty.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> does not start with a slash (<c>/</c>) character.</para>
/// </exception>
/// <seealso cref="Utilities.UrlPath.Normalize"/>
public static string UrlPath(string argumentName, string value, bool isBasePath)
{
var exception = Utilities.UrlPath.ValidateInternal(argumentName, value);
if (exception != null)
throw exception;
return Utilities.UrlPath.Normalize(value, isBasePath);
}
/// <summary>
/// Ensures that the value of an argument is a valid local path
/// and, optionally, gets the corresponding full path.
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <param name="getFullPath"><see langword="true"/> to get the full path, <see langword="false"/> to leave the path as is..</param>
/// <returns>The local path, or the full path if <paramref name="getFullPath"/> is <see langword="true"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is empty.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> contains only white space.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> contains one or more invalid characters.</para>
/// <para>- or -</para>
/// <para><paramref name="getFullPath"/> is <see langword="true"/> and the full path could not be obtained.</para>
/// </exception>
public static string LocalPath(string argumentName, string value, bool getFullPath)
{
if (value == null)
throw new ArgumentNullException(argumentName);
if (value.Length == 0)
throw new ArgumentException("Local path is empty.", argumentName);
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Local path contains only white space.", argumentName);
if (value.IndexOfAny(InvalidLocalPathChars) >= 0)
throw new ArgumentException("Local path contains one or more invalid characters.", argumentName);
if (getFullPath)
{
try
{
value = Path.GetFullPath(value);
}
catch (Exception e) when (e is ArgumentException || e is SecurityException || e is NotSupportedException || e is PathTooLongException)
{
throw new ArgumentException("Could not get the full local path.", argumentName, e);
}
}
return value;
}
private static char[] GetInvalidLocalPathChars()
{
var systemChars = Path.GetInvalidPathChars();
var p = systemChars.Length;
var result = new char[p + 2];
Array.Copy(systemChars, result, p);
result[p++] = '*';
result[p] = '?';
return result;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Linq;
namespace EmbedIO.Utilities
{
partial class Validate
{
private static readonly char[] ValidRfc2616TokenChars = GetValidRfc2616TokenChars();
/// <summary>
/// <para>Ensures that a <see langword="string"/> argument is valid as a token as defined by
/// <see href="https://tools.ietf.org/html/rfc2616#section-2.2">RFC2616, Section 2.2</see>.</para>
/// <para>RFC2616 tokens are used, for example, as:</para>
/// <list type="bullet">
/// <item><description>cookie names, as stated in <see href="https://tools.ietf.org/html/rfc6265#section-4.1.1">RFC6265, Section 4.1.1</see>;</description></item>
/// <item><description>WebSocket protocol names, as stated in <see href="https://tools.ietf.org/html/rfc6455#section-4.3">RFC6455, Section 4.3</see>.</description></item>
/// </list>
/// <para>Only a restricted set of characters are allowed in tokens, including:</para>
/// <list type="bullet">
/// <item><description>upper- and lower-case letters of the English alphabet;</description></item>
/// <item><description>decimal digits;</description></item>
/// <item><description>the following non-alphanumeric characters:
/// <c>!</c>, <c>#</c>, <c>$</c>, <c>%</c>, <c>&amp;</c>, <c>'</c>, <c>*</c>, <c>+</c>,
/// <c>-</c>, <c>.</c>, <c>^</c>, <c>_</c>, <c>`</c>, <c>|</c>, <c>~</c>.</description></item>
/// </list>
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <returns><paramref name="value"/>, if it is a valid token.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is the empty string.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> contains one or more characters that are not allowed in a token.</para>
/// </exception>
public static string Rfc2616Token(string argumentName, string value)
{
value = NotNullOrEmpty(argumentName, value);
if (!IsRfc2616Token(value))
throw new ArgumentException("Token contains one or more invalid characters.", argumentName);
return value;
}
internal static bool IsRfc2616Token(string value)
=> !string.IsNullOrEmpty(value)
&& !value.Any(c => c < '\x21' || c > '\x7E' || Array.BinarySearch(ValidRfc2616TokenChars, c) < 0);
private static char[] GetValidRfc2616TokenChars()
=> "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&'*+-.^_`|~"
.OrderBy(c => c)
.ToArray();
}
}

View File

@@ -0,0 +1,33 @@
using System;
namespace EmbedIO.Utilities
{
partial class Validate
{
/// <summary>
/// Ensures that the value of an argument is a valid route.
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <param name="isBaseRoute"><see langword="true"/> if the argument must be a base route;
/// <see langword="false"/> if the argument must be a non-base route.</param>
/// <returns><paramref name="value"/>, if it is a valid route.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is empty.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> does not start with a slash (<c>/</c>) character.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> does not comply with route syntax.</para>
/// </exception>
/// <seealso cref="Routing.Route.IsValid"/>
public static string Route(string argumentName, string value, bool isBaseRoute)
{
var exception = Routing.Route.ValidateInternal(argumentName, value, isBaseRoute);
if (exception != null)
throw exception;
return Utilities.UrlPath.UnsafeNormalize(value, isBaseRoute);
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
namespace EmbedIO.Utilities
{
/// <summary>
/// Provides validation methods for method arguments.
/// </summary>
public static partial class Validate
{
/// <summary>
/// Ensures that an argument is not <see langword="null"/>.
/// </summary>
/// <typeparam name="T">The type of the argument to validate.</typeparam>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <returns><paramref name="value"/> if not <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
public static T NotNull<T>(string argumentName, T? value)
where T : class
=> value ?? throw new ArgumentNullException(argumentName);
/// <summary>
/// Ensures that a <see langword="string"/> argument is neither <see langword="null"/> nor the empty string.
/// </summary>
/// <param name="argumentName">The name of the argument to validate.</param>
/// <param name="value">The value to validate.</param>
/// <returns><paramref name="value"/> if neither <see langword="null"/> nor the empty string.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="value"/> is the empty string.</exception>
public static string NotNullOrEmpty(string argumentName, string? value)
{
if (value == null)
throw new ArgumentNullException(argumentName);
if (value.Length == 0)
throw new ArgumentException("String is empty.", argumentName);
return value;
}
/// <summary>
/// Ensures that a valid URL can be constructed from a <see langword="string"/> argument.
/// </summary>
/// <param name="argumentName">Name of the argument.</param>
/// <param name="value">The value.</param>
/// <param name="uriKind">Specifies whether <paramref name="value"/> is a relative URL, absolute URL, or is indeterminate.</param>
/// <param name="enforceHttp">Ensure that, if <paramref name="value"/> is an absolute URL, its scheme is either <c>http</c> or <c>https</c>.</param>
/// <returns>The string representation of the constructed URL.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="value"/> is not a valid URL.</para>
/// <para>- or -</para>
/// <para><paramref name="enforceHttp"/> is <see langword="true"/>, <paramref name="value"/> is an absolute URL,
/// and <paramref name="value"/>'s scheme is neither <c>http</c> nor <c>https</c>.</para>
/// </exception>
/// <seealso cref="Url(string,string,Uri,bool)"/>
public static string Url(
string argumentName,
string value,
UriKind uriKind = UriKind.RelativeOrAbsolute,
bool enforceHttp = false)
{
Uri uri;
try
{
uri = new Uri(NotNull(argumentName, value), uriKind);
}
catch (UriFormatException e)
{
throw new ArgumentException("URL is not valid.", argumentName, e);
}
if (enforceHttp && uri.IsAbsoluteUri && uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
throw new ArgumentException("URL scheme is neither HTTP nor HTTPS.", argumentName);
return uri.ToString();
}
/// <summary>
/// Ensures that a valid URL, either absolute or relative to the given <paramref name="baseUri"/>,
/// can be constructed from a <see langword="string"/> argument and returns the absolute URL
/// obtained by combining <paramref name="baseUri"/> and <paramref name="value"/>.
/// </summary>
/// <param name="argumentName">Name of the argument.</param>
/// <param name="value">The value.</param>
/// <param name="baseUri">The base URI for relative URLs.</param>
/// <param name="enforceHttp">Ensure that the resulting URL's scheme is either <c>http</c> or <c>https</c>.</param>
/// <returns>The string representation of the constructed URL.</returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="baseUri"/> is <see langword="null"/>.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> is <see langword="null"/>.</para>
/// </exception>
/// <exception cref="ArgumentException">
/// <para><paramref name="baseUri"/> is not an absolute URI.</para>
/// <para>- or -</para>
/// <para><paramref name="value"/> is not a valid URL.</para>
/// <para>- or -</para>
/// <para><paramref name="enforceHttp"/> is <see langword="true"/>,
/// and the combination of <paramref name="baseUri"/> and <paramref name="value"/> has a scheme
/// that is neither <c>http</c> nor <c>https</c>.</para>
/// </exception>
/// <seealso cref="Url(string,string,UriKind,bool)"/>
public static string Url(string argumentName, string value, Uri baseUri, bool enforceHttp = false)
{
if (!NotNull(nameof(baseUri), baseUri).IsAbsoluteUri)
throw new ArgumentException("Base URI is not an absolute URI.", nameof(baseUri));
Uri uri;
try
{
uri = new Uri(baseUri, new Uri(NotNull(argumentName, value), UriKind.RelativeOrAbsolute));
}
catch (UriFormatException e)
{
throw new ArgumentException("URL is not valid.", argumentName, e);
}
if (enforceHttp && uri.IsAbsoluteUri && uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
throw new ArgumentException("URL scheme is neither HTTP nor HTTPS.", argumentName);
return uri.ToString();
}
}
}