jellyfin/SocketHttpListener/Ext.cs
Erwin de Haan ec1f5dc317 Mayor code cleanup
Add Argument*Exceptions now use proper nameof operators.

Added exception messages to quite a few Argument*Exceptions.

Fixed rethorwing to be proper syntax.

Added a ton of null checkes. (This is only a start, there are about 500 places that need proper null handling)

Added some TODOs to log certain exceptions.

Fix sln again.

Fixed all AssemblyInfo's and added proper copyright (where I could find them)

We live in *current year*.

Fixed the use of braces.

Fixed a ton of properties, and made a fair amount of functions static that should be and can be static.

Made more Methods that should be static static.

You can now use static to find bad functions!

Removed unused variable. And added one more proper XML comment.
2019-01-10 20:38:53 +01:00

961 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Services;
using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode;
using WebSocketState = System.Net.WebSockets.WebSocketState;
namespace SocketHttpListener
{
/// <summary>
/// Provides a set of static methods for the websocket-sharp.
/// </summary>
public static class Ext
{
#region Private Const Fields
private const string _tspecials = "()<>@,;:\\\"/[]?={} \t";
#endregion
#region Private Methods
private static MemoryStream compress(this Stream stream)
{
var output = new MemoryStream();
if (stream.Length == 0)
return output;
stream.Position = 0;
using (var ds = new DeflateStream(output, CompressionMode.Compress, true))
{
stream.CopyTo(ds);
//ds.Close(); // "BFINAL" set to 1.
output.Position = 0;
return output;
}
}
private static byte[] decompress(this byte[] value)
{
if (value.Length == 0)
return value;
using (var input = new MemoryStream(value))
{
return input.decompressToArray();
}
}
private static MemoryStream decompress(this Stream stream)
{
var output = new MemoryStream();
if (stream.Length == 0)
return output;
stream.Position = 0;
using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true))
{
ds.CopyTo(output, true);
return output;
}
}
private static byte[] decompressToArray(this Stream stream)
{
using (var decomp = stream.decompress())
{
return decomp.ToArray();
}
}
private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length)
{
var len = stream.Read(buffer, offset, length);
if (len < 1)
return buffer.SubArray(0, offset);
var tmp = 0;
while (len < length)
{
tmp = stream.Read(buffer, offset + len, length - len);
if (tmp < 1)
break;
len += tmp;
}
return len < length
? buffer.SubArray(0, offset + len)
: buffer;
}
private static bool readBytes(
this Stream stream, byte[] buffer, int offset, int length, Stream dest)
{
var bytes = stream.readBytes(buffer, offset, length);
var len = bytes.Length;
dest.Write(bytes, 0, len);
return len == offset + length;
}
#endregion
#region Internal Methods
internal static byte[] Append(this ushort code, string reason)
{
using (var buffer = new MemoryStream())
{
var tmp = code.ToByteArrayInternally(ByteOrder.Big);
buffer.Write(tmp, 0, 2);
if (reason != null && reason.Length > 0)
{
tmp = Encoding.UTF8.GetBytes(reason);
buffer.Write(tmp, 0, tmp.Length);
}
return buffer.ToArray();
}
}
internal static string CheckIfClosable(this WebSocketState state)
{
return state == WebSocketState.CloseSent
? "While closing the WebSocket connection."
: state == WebSocketState.Closed
? "The WebSocket connection has already been closed."
: null;
}
internal static string CheckIfOpen(this WebSocketState state)
{
return state == WebSocketState.Connecting
? "A WebSocket connection isn't established."
: state == WebSocketState.CloseSent
? "While closing the WebSocket connection."
: state == WebSocketState.Closed
? "The WebSocket connection has already been closed."
: null;
}
internal static string CheckIfValidControlData(this byte[] data, string paramName)
{
return data.Length > 125
? string.Format("'{0}' length must be less.", paramName)
: null;
}
internal static Stream Compress(this Stream stream, CompressionMethod method)
{
return method == CompressionMethod.Deflate
? stream.compress()
: stream;
}
internal static bool Contains<T>(this IEnumerable<T> source, Func<T, bool> condition)
{
foreach (T elm in source)
if (condition(elm))
return true;
return false;
}
internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition)
{
var readLen = 0;
var bufferLen = 256;
var buffer = new byte[bufferLen];
while ((readLen = src.Read(buffer, 0, bufferLen)) > 0)
{
dest.Write(buffer, 0, readLen);
}
if (setDefaultPosition)
dest.Position = 0;
}
internal static byte[] Decompress(this byte[] value, CompressionMethod method)
{
return method == CompressionMethod.Deflate
? value.decompress()
: value;
}
internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method)
{
return method == CompressionMethod.Deflate
? stream.decompressToArray()
: stream.ToByteArray();
}
/// <summary>
/// Determines whether the specified <see cref="int"/> equals the specified <see cref="char"/>,
/// and invokes the specified Action&lt;int&gt; delegate at the same time.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="value"/> equals <paramref name="c"/>;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="value">
/// An <see cref="int"/> to compare.
/// </param>
/// <param name="c">
/// A <see cref="char"/> to compare.
/// </param>
/// <param name="action">
/// An Action&lt;int&gt; delegate that references the method(s) called at
/// the same time as comparing. An <see cref="int"/> parameter to pass to
/// the method(s) is <paramref name="value"/>.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="value"/> isn't between 0 and 255.
/// </exception>
internal static bool EqualsWith(this int value, char c, Action<int> action)
{
if (value < 0 || value > 255)
throw new ArgumentOutOfRangeException(nameof(value));
action(value);
return value == c - 0;
}
internal static string GetMessage(this CloseStatusCode code)
{
return code == CloseStatusCode.ProtocolError
? "A WebSocket protocol error has occurred."
: code == CloseStatusCode.IncorrectData
? "An incorrect data has been received."
: code == CloseStatusCode.Abnormal
? "An exception has occurred."
: code == CloseStatusCode.InconsistentData
? "An inconsistent data has been received."
: code == CloseStatusCode.PolicyViolation
? "A policy violation has occurred."
: code == CloseStatusCode.TooBig
? "A too big data has been received."
: code == CloseStatusCode.IgnoreExtension
? "WebSocket client did not receive expected extension(s)."
: code == CloseStatusCode.ServerError
? "WebSocket server got an internal error."
: code == CloseStatusCode.TlsHandshakeFailure
? "An error has occurred while handshaking."
: string.Empty;
}
internal static string GetNameInternal(this string nameAndValue, string separator)
{
var i = nameAndValue.IndexOf(separator);
return i > 0
? nameAndValue.Substring(0, i).Trim()
: null;
}
internal static string GetValueInternal(this string nameAndValue, string separator)
{
var i = nameAndValue.IndexOf(separator);
return i >= 0 && i < nameAndValue.Length - 1
? nameAndValue.Substring(i + 1).Trim()
: null;
}
internal static bool IsCompressionExtension(this string value, CompressionMethod method)
{
return value.StartsWith(method.ToExtensionString());
}
internal static bool IsPortNumber(this int value)
{
return value > 0 && value < 65536;
}
internal static bool IsReserved(this ushort code)
{
return code == (ushort)CloseStatusCode.Undefined ||
code == (ushort)CloseStatusCode.NoStatusCode ||
code == (ushort)CloseStatusCode.Abnormal ||
code == (ushort)CloseStatusCode.TlsHandshakeFailure;
}
internal static bool IsReserved(this CloseStatusCode code)
{
return code == CloseStatusCode.Undefined ||
code == CloseStatusCode.NoStatusCode ||
code == CloseStatusCode.Abnormal ||
code == CloseStatusCode.TlsHandshakeFailure;
}
internal static bool IsText(this string value)
{
var len = value.Length;
for (var i = 0; i < len; i++)
{
char c = value[i];
if (c < 0x20 && !"\r\n\t".Contains(c))
return false;
if (c == 0x7f)
return false;
if (c == '\n' && ++i < len)
{
c = value[i];
if (!" \t".Contains(c))
return false;
}
}
return true;
}
internal static bool IsToken(this string value)
{
foreach (char c in value)
if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c))
return false;
return true;
}
internal static string Quote(this string value)
{
return value.IsToken()
? value
: string.Format("\"{0}\"", value.Replace("\"", "\\\""));
}
internal static byte[] ReadBytes(this Stream stream, int length)
{
return stream.readBytes(new byte[length], 0, length);
}
internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength)
{
using (var result = new MemoryStream())
{
var count = length / bufferLength;
var rem = (int)(length % bufferLength);
var buffer = new byte[bufferLength];
var end = false;
for (long i = 0; i < count; i++)
{
if (!stream.readBytes(buffer, 0, bufferLength, result))
{
end = true;
break;
}
}
if (!end && rem > 0)
stream.readBytes(new byte[rem], 0, rem, result);
return result.ToArray();
}
}
internal static async Task<byte[]> ReadBytesAsync(this Stream stream, int length)
{
var buffer = new byte[length];
var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false);
var bytes = len < 1
? new byte[0]
: len < length
? stream.readBytes(buffer, len, length - len)
: buffer;
return bytes;
}
internal static string RemovePrefix(this string value, params string[] prefixes)
{
var i = 0;
foreach (var prefix in prefixes)
{
if (value.StartsWith(prefix))
{
i = prefix.Length;
break;
}
}
return i > 0
? value.Substring(i)
: value;
}
internal static T[] Reverse<T>(this T[] array)
{
var len = array.Length;
T[] reverse = new T[len];
var end = len - 1;
for (var i = 0; i <= end; i++)
reverse[i] = array[end - i];
return reverse;
}
internal static IEnumerable<string> SplitHeaderValue(
this string value, params char[] separator)
{
var len = value.Length;
var separators = new string(separator);
var buffer = new StringBuilder(32);
var quoted = false;
var escaped = false;
char c;
for (var i = 0; i < len; i++)
{
c = value[i];
if (c == '"')
{
if (escaped)
escaped = !escaped;
else
quoted = !quoted;
}
else if (c == '\\')
{
if (i < len - 1 && value[i + 1] == '"')
escaped = true;
}
else if (separators.Contains(c))
{
if (!quoted)
{
yield return buffer.ToString();
buffer.Length = 0;
continue;
}
}
else {
}
buffer.Append(c);
}
if (buffer.Length > 0)
yield return buffer.ToString();
}
internal static byte[] ToByteArray(this Stream stream)
{
using (var output = new MemoryStream())
{
stream.Position = 0;
stream.CopyTo(output);
return output.ToArray();
}
}
internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
Array.Reverse(bytes);
return bytes;
}
internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
Array.Reverse(bytes);
return bytes;
}
internal static string ToExtensionString(
this CompressionMethod method, params string[] parameters)
{
if (method == CompressionMethod.None)
return string.Empty;
var m = string.Format("permessage-{0}", method.ToString().ToLower());
if (parameters == null || parameters.Length == 0)
return m;
return string.Format("{0}; {1}", m, parameters.ToString("; "));
}
internal static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
{
return new List<TSource>(source);
}
internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder)
{
return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0);
}
internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder)
{
return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0);
}
internal static string TrimEndSlash(this string value)
{
value = value.TrimEnd('/');
return value.Length > 0
? value
: "/";
}
internal static string Unquote(this string value)
{
var start = value.IndexOf('\"');
var end = value.LastIndexOf('\"');
if (start < end)
value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\"");
return value.Trim();
}
internal static void WriteBytes(this Stream stream, byte[] value)
{
using (var src = new MemoryStream(value))
{
src.CopyTo(stream);
}
}
#endregion
#region Public Methods
/// <summary>
/// Determines whether the specified <see cref="string"/> contains any of characters
/// in the specified array of <see cref="char"/>.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="value"/> contains any of <paramref name="chars"/>;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="value">
/// A <see cref="string"/> to test.
/// </param>
/// <param name="chars">
/// An array of <see cref="char"/> that contains characters to find.
/// </param>
public static bool Contains(this string value, params char[] chars)
{
return chars == null || chars.Length == 0
? true
: value == null || value.Length == 0
? false
: value.IndexOfAny(chars) != -1;
}
/// <summary>
/// Determines whether the specified <see cref="QueryParamCollection"/> contains the entry
/// with the specified <paramref name="name"/>.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="collection"/> contains the entry
/// with <paramref name="name"/>; otherwise, <c>false</c>.
/// </returns>
/// <param name="collection">
/// A <see cref="QueryParamCollection"/> to test.
/// </param>
/// <param name="name">
/// A <see cref="string"/> that represents the key of the entry to find.
/// </param>
public static bool Contains(this QueryParamCollection collection, string name)
{
return collection == null || collection.Count == 0
? false
: collection[name] != null;
}
/// <summary>
/// Determines whether the specified <see cref="QueryParamCollection"/> contains the entry
/// with the specified both <paramref name="name"/> and <paramref name="value"/>.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="collection"/> contains the entry
/// with both <paramref name="name"/> and <paramref name="value"/>;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="collection">
/// A <see cref="QueryParamCollection"/> to test.
/// </param>
/// <param name="name">
/// A <see cref="string"/> that represents the key of the entry to find.
/// </param>
/// <param name="value">
/// A <see cref="string"/> that represents the value of the entry to find.
/// </param>
public static bool Contains(this QueryParamCollection collection, string name, string value)
{
if (collection == null || collection.Count == 0)
return false;
var values = collection[name];
if (values == null)
return false;
foreach (var v in values.Split(','))
if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
/// <summary>
/// Emits the specified <c>EventHandler&lt;TEventArgs&gt;</c> delegate
/// if it isn't <see langword="null"/>.
/// </summary>
/// <param name="eventHandler">
/// An <c>EventHandler&lt;TEventArgs&gt;</c> to emit.
/// </param>
/// <param name="sender">
/// An <see cref="object"/> from which emits this <paramref name="eventHandler"/>.
/// </param>
/// <param name="e">
/// A <c>TEventArgs</c> that represents the event data.
/// </param>
/// <typeparam name="TEventArgs">
/// The type of the event data generated by the event.
/// </typeparam>
public static void Emit<TEventArgs>(
this EventHandler<TEventArgs> eventHandler, object sender, TEventArgs e)
where TEventArgs : EventArgs
{
if (eventHandler != null)
eventHandler(sender, e);
}
/// <summary>
/// Gets the description of the specified HTTP status <paramref name="code"/>.
/// </summary>
/// <returns>
/// A <see cref="string"/> that represents the description of the HTTP status code.
/// </returns>
/// <param name="code">
/// One of <see cref="HttpStatusCode"/> enum values, indicates the HTTP status codes.
/// </param>
public static string GetDescription(this HttpStatusCode code)
{
return ((int)code).GetStatusDescription();
}
/// <summary>
/// Gets the description of the specified HTTP status <paramref name="code"/>.
/// </summary>
/// <returns>
/// A <see cref="string"/> that represents the description of the HTTP status code.
/// </returns>
/// <param name="code">
/// An <see cref="int"/> that represents the HTTP status code.
/// </param>
public static string GetStatusDescription(this int code)
{
switch (code)
{
case 100: return "Continue";
case 101: return "Switching Protocols";
case 102: return "Processing";
case 200: return "OK";
case 201: return "Created";
case 202: return "Accepted";
case 203: return "Non-Authoritative Information";
case 204: return "No Content";
case 205: return "Reset Content";
case 206: return "Partial Content";
case 207: return "Multi-Status";
case 300: return "Multiple Choices";
case 301: return "Moved Permanently";
case 302: return "Found";
case 303: return "See Other";
case 304: return "Not Modified";
case 305: return "Use Proxy";
case 307: return "Temporary Redirect";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 402: return "Payment Required";
case 403: return "Forbidden";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 406: return "Not Acceptable";
case 407: return "Proxy Authentication Required";
case 408: return "Request Timeout";
case 409: return "Conflict";
case 410: return "Gone";
case 411: return "Length Required";
case 412: return "Precondition Failed";
case 413: return "Request Entity Too Large";
case 414: return "Request-Uri Too Long";
case 415: return "Unsupported Media Type";
case 416: return "Requested Range Not Satisfiable";
case 417: return "Expectation Failed";
case 422: return "Unprocessable Entity";
case 423: return "Locked";
case 424: return "Failed Dependency";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
case 502: return "Bad Gateway";
case 503: return "Service Unavailable";
case 504: return "Gateway Timeout";
case 505: return "Http Version Not Supported";
case 507: return "Insufficient Storage";
}
return string.Empty;
}
/// <summary>
/// Determines whether the specified <see cref="ByteOrder"/> is host
/// (this computer architecture) byte order.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="order"/> is host byte order;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="order">
/// One of the <see cref="ByteOrder"/> enum values, to test.
/// </param>
public static bool IsHostOrder(this ByteOrder order)
{
// true : !(true ^ true) or !(false ^ false)
// false: !(true ^ false) or !(false ^ true)
return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little));
}
/// <summary>
/// Determines whether the specified <see cref="string"/> is a predefined scheme.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="value"/> is a predefined scheme; otherwise, <c>false</c>.
/// </returns>
/// <param name="value">
/// A <see cref="string"/> to test.
/// </param>
public static bool IsPredefinedScheme(this string value)
{
if (value == null || value.Length < 2)
return false;
var c = value[0];
if (c == 'h')
return value == "http" || value == "https";
if (c == 'w')
return value == "ws" || value == "wss";
if (c == 'f')
return value == "file" || value == "ftp";
if (c == 'n')
{
c = value[1];
return c == 'e'
? value == "news" || value == "net.pipe" || value == "net.tcp"
: value == "nntp";
}
return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto");
}
/// <summary>
/// Determines whether the specified <see cref="string"/> is a URI string.
/// </summary>
/// <returns>
/// <c>true</c> if <paramref name="value"/> may be a URI string; otherwise, <c>false</c>.
/// </returns>
/// <param name="value">
/// A <see cref="string"/> to test.
/// </param>
public static bool MaybeUri(this string value)
{
if (value == null || value.Length == 0)
return false;
var i = value.IndexOf(':');
if (i == -1)
return false;
if (i >= 10)
return false;
return value.Substring(0, i).IsPredefinedScheme();
}
/// <summary>
/// Retrieves a sub-array from the specified <paramref name="array"/>.
/// A sub-array starts at the specified element position.
/// </summary>
/// <returns>
/// An array of T that receives a sub-array, or an empty array of T if any problems
/// with the parameters.
/// </returns>
/// <param name="array">
/// An array of T that contains the data to retrieve a sub-array.
/// </param>
/// <param name="startIndex">
/// An <see cref="int"/> that contains the zero-based starting position of a sub-array
/// in <paramref name="array"/>.
/// </param>
/// <param name="length">
/// An <see cref="int"/> that contains the number of elements to retrieve a sub-array.
/// </param>
/// <typeparam name="T">
/// The type of elements in the <paramref name="array"/>.
/// </typeparam>
public static T[] SubArray<T>(this T[] array, int startIndex, int length)
{
if (array == null || array.Length == 0)
return new T[0];
if (startIndex < 0 || length <= 0)
return new T[0];
if (startIndex + length > array.Length)
return new T[0];
if (startIndex == 0 && array.Length == length)
return array;
T[] subArray = new T[length];
Array.Copy(array, startIndex, subArray, 0, length);
return subArray;
}
/// <summary>
/// Converts the order of the specified array of <see cref="byte"/> to the host byte order.
/// </summary>
/// <returns>
/// An array of <see cref="byte"/> converted from <paramref name="src"/>.
/// </returns>
/// <param name="src">
/// An array of <see cref="byte"/> to convert.
/// </param>
/// <param name="srcOrder">
/// One of the <see cref="ByteOrder"/> enum values, indicates the byte order of
/// <paramref name="src"/>.
/// </param>
/// <exception cref="ArgumentNullException">
/// <paramref name="src"/> is <see langword="null"/>.
/// </exception>
public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder)
{
if (src == null)
throw new ArgumentNullException(nameof(src));
return src.Length > 1 && !srcOrder.IsHostOrder()
? src.Reverse()
: src;
}
/// <summary>
/// Converts the specified <paramref name="array"/> to a <see cref="string"/> that
/// concatenates the each element of <paramref name="array"/> across the specified
/// <paramref name="separator"/>.
/// </summary>
/// <returns>
/// A <see cref="string"/> converted from <paramref name="array"/>,
/// or <see cref="String.Empty"/> if <paramref name="array"/> is empty.
/// </returns>
/// <param name="array">
/// An array of T to convert.
/// </param>
/// <param name="separator">
/// A <see cref="string"/> that represents the separator string.
/// </param>
/// <typeparam name="T">
/// The type of elements in <paramref name="array"/>.
/// </typeparam>
/// <exception cref="ArgumentNullException">
/// <paramref name="array"/> is <see langword="null"/>.
/// </exception>
public static string ToString<T>(this T[] array, string separator)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
var len = array.Length;
if (len == 0)
return string.Empty;
if (separator == null)
separator = string.Empty;
var buff = new StringBuilder(64);
(len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator));
buff.Append(array[len - 1].ToString());
return buff.ToString();
}
/// <summary>
/// Executes the specified <c>Action&lt;int&gt;</c> delegate <paramref name="n"/> times.
/// </summary>
/// <param name="n">
/// An <see cref="int"/> is the number of times to execute.
/// </param>
/// <param name="action">
/// An <c>Action&lt;int&gt;</c> delegate that references the method(s) to execute.
/// An <see cref="int"/> parameter to pass to the method(s) is the zero-based count of
/// iteration.
/// </param>
public static void Times(this int n, Action<int> action)
{
if (n > 0 && action != null)
for (int i = 0; i < n; i++)
action(i);
}
/// <summary>
/// Converts the specified <see cref="string"/> to a <see cref="Uri"/>.
/// </summary>
/// <returns>
/// A <see cref="Uri"/> converted from <paramref name="uriString"/>, or <see langword="null"/>
/// if <paramref name="uriString"/> isn't successfully converted.
/// </returns>
/// <param name="uriString">
/// A <see cref="string"/> to convert.
/// </param>
public static Uri ToUri(this string uriString)
{
Uri res;
return Uri.TryCreate(
uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out res)
? res
: null;
}
/// <summary>
/// URL-decodes the specified <see cref="string"/>.
/// </summary>
/// <returns>
/// A <see cref="string"/> that receives the decoded string, or the <paramref name="value"/>
/// if it's <see langword="null"/> or empty.
/// </returns>
/// <param name="value">
/// A <see cref="string"/> to decode.
/// </param>
public static string UrlDecode(this string value)
{
return value == null || value.Length == 0
? value
: WebUtility.UrlDecode(value);
}
#endregion
}
}