/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace OpenIddict.Extensions;
///
/// Exposes common polyfills used by the OpenIddict assemblies.
///
internal static class OpenIddictPolyfills
{
extension(ArgumentOutOfRangeException)
{
/// Throws an if is negative.
/// The argument to validate as non-negative.
/// The name of the parameter with which corresponds.
public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null)
where T : struct, IComparable
{
switch (value)
{
case byte or ushort or uint or ulong or char:
return;
case sbyte n:
if (n < 0)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case short n:
if (n < 0)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case int n:
if (n < 0)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case long n:
if (n < 0L)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case float n:
if (n < 0F)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case double n:
if (n < 0D)
ThrowArgumentOutOfRangeException(paramName, value);
return;
case decimal n:
if (n < 0M)
ThrowArgumentOutOfRangeException(paramName, value);
return;
default:
throw new InvalidOperationException($"Invalid type '{typeof(T).AssemblyQualifiedName}' for {paramName}.");
}
static void ThrowArgumentOutOfRangeException(string? paramName, object value)
{
throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must not be negative.");
}
}
}
extension(Convert)
{
#if !SUPPORTS_HEXADECIMAL_STRING_CONVERSION
/// Converts the specified string, which encodes binary data as hex characters, to an equivalent 8-bit unsigned integer array.
/// The string to convert.
/// An array of 8-bit unsigned integers that is equivalent to .
/// is null.
/// The length of , is not zero or a multiple of 2.
/// The format of is invalid. contains a non-hex character.
public static byte[] FromHexString(string s)
{
if ((uint) s.Length % 2 is not 0)
{
throw new FormatException(SR.GetResourceString(SR.ID0413));
}
var array = new byte[s.Length / 2];
for (var index = 0; index < s.Length; index += 2)
{
array[index / 2] = Convert.ToByte(s.Substring(index, 2), 16);
}
return array;
}
#endif
}
extension(IEnumerable source)
{
#if !SUPPORTS_CHUNK_LINQ_EXTENSION
///
/// Split the elements of a sequence into chunks of size at most .
///
///
/// Every chunk except the last will be of size .
/// The last chunk will contain the remaining elements and may be of a smaller size.
///
/// Maximum size of each chunk.
///
/// An that contains the elements of the input
/// sequence split into chunks of size .
///
public IEnumerable Chunk(int size)
{
// Note: this polyfill was directly copied from .NET's source code:
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Chunk.cs.
using IEnumerator enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
var count = Math.Min(size, 4);
int index;
do
{
var array = new TSource[count];
array[0] = enumerator.Current;
index = 1;
if (size != array.Length)
{
for (; index < size && enumerator.MoveNext(); index++)
{
if (index >= array.Length)
{
count = (int) Math.Min((uint) size, 2 * (uint) array.Length);
Array.Resize(ref array, count);
}
array[index] = enumerator.Current;
}
}
else
{
var local = array;
Debug.Assert(local.Length == size);
for (; (uint) index < (uint) local.Length && enumerator.MoveNext(); index++)
{
local[index] = enumerator.Current;
}
}
if (index != array.Length)
{
Array.Resize(ref array, index);
}
yield return array;
}
while (index >= size && enumerator.MoveNext());
}
}
#endif
}
extension(OperatingSystem)
{
#if !SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON
///
/// Indicates whether the current application is running on Android.
///
public static bool IsAndroid() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID"));
///
/// Check for the Android API level (returned by 'ro.build.version.sdk') with a >=
/// version comparison. Used to guard APIs that were added in the given Android release.
///
public static bool IsAndroidVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
=> IsAndroid() && IsOSVersionAtLeast(major, minor, build, revision);
///
/// Indicates whether the current application is running on iOS or MacCatalyst.
///
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOS() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS"));
///
/// Check for the iOS/MacCatalyst version (returned by 'libobjc.get_operatingSystemVersion')
/// with a >= version comparison. Used to guard APIs that were added in the given iOS release.
///
[SupportedOSPlatformGuard("maccatalyst")]
public static bool IsIOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsIOS() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on Linux.
///
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
///
/// Indicates whether the current application is running on Mac Catalyst.
///
public static bool IsMacCatalyst() => RuntimeInformation.IsOSPlatform(OSPlatform.Create("MACCATALYST"));
///
/// Check for the Mac Catalyst version (iOS version as presented in Apple documentation) with a >=
/// version comparison. Used to guard APIs that were added in the given Mac Catalyst release.
///
public static bool IsMacCatalystVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacCatalyst() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on macOS.
///
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
///
/// Check for the macOS version (returned by 'libobjc.get_operatingSystemVersion') with a >=
/// version comparison. Used to guard APIs that were added in the given macOS release.
///
public static bool IsMacOSVersionAtLeast(int major, int minor = 0, int build = 0)
=> IsMacOS() && IsOSVersionAtLeast(major, minor, build, 0);
///
/// Indicates whether the current application is running on Windows.
///
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
///
/// Check for the Windows version (returned by 'RtlGetVersion') with a >= version
/// comparison. Used to guard APIs that were added in the given Windows release.
///
public static bool IsWindowsVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
{
if (Environment.OSVersion.Platform is PlatformID.Win32NT &&
Environment.OSVersion.Version >= new Version(major, minor, build, revision))
{
return true;
}
// Note: on older versions of .NET, Environment.OSVersion.Version is known to be affected by
// the compatibility shims used by Windows 10+ when the application doesn't have a manifest
// that explicitly indicates it's compatible with Windows 10 and higher. To avoid that, a
// second pass using RuntimeInformation.OSDescription (that calls NtDll.RtlGetVersion() under
// the hood) is made. Note: no version is returned on UWP due to the missing Win32 API.
return RuntimeInformation.OSDescription.StartsWith("Microsoft Windows ", StringComparison.OrdinalIgnoreCase) &&
RuntimeInformation.OSDescription["Microsoft Windows ".Length..] is string value &&
Version.TryParse(value, out Version? version) && version >= new Version(major, minor, build, revision);
}
#endif
}
extension(ValueTask)
{
#if !SUPPORTS_VALUETASK_COMPLETED_TASK
///
/// Gets a task that has already completed successfully.
///
public static ValueTask CompletedTask => default;
#endif
}
extension(ValueTask)
{
#if !SUPPORTS_VALUETASK_COMPLETED_TASK
///
/// Gets a task that has already completed successfully.
///
public static ValueTask CompletedTask => default;
#endif
}
#if !SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON
static bool IsOSVersionAtLeast(int major, int minor, int build, int revision)
{
Version current = Environment.OSVersion.Version;
if (current.Major != major)
{
return current.Major > major;
}
if (current.Minor != minor)
{
return current.Minor > minor;
}
int currentBuild = current.Build < 0 ? 0 : current.Build;
build = build < 0 ? 0 : build;
if (currentBuild != build)
{
return currentBuild > build;
}
int currentRevision = current.Revision < 0 ? 0 : current.Revision;
revision = revision < 0 ? 0 : revision;
return currentRevision >= revision;
}
#endif
extension(X509ChainPolicy policy)
{
#if !SUPPORTS_X509_CHAIN_POLICY_CLONING
public X509ChainPolicy Clone()
{
var clone = new X509ChainPolicy
{
#if SUPPORTS_X509_CHAIN_POLICY_DOWNLOAD_MODE
DisableCertificateDownloads = policy.DisableCertificateDownloads,
#endif
RevocationMode = policy.RevocationMode,
RevocationFlag = policy.RevocationFlag,
#if SUPPORTS_X509_CHAIN_POLICY_TRUST_MODE
TrustMode = policy.TrustMode,
#endif
UrlRetrievalTimeout = policy.UrlRetrievalTimeout,
VerificationFlags = policy.VerificationFlags,
VerificationTime = policy.VerificationTime,
#if SUPPORTS_X509_CHAIN_POLICY_VERIFICATION_TIME_MODE
VerificationTimeIgnored = policy.VerificationTimeIgnored
#endif
};
if (policy.ApplicationPolicy.Count is > 0)
{
for (var index = 0; index < policy.ApplicationPolicy.Count; index++)
{
clone.ApplicationPolicy.Add(policy.ApplicationPolicy[index]);
}
}
if (policy.CertificatePolicy.Count is > 0)
{
for (var index = 0; index < policy.CertificatePolicy.Count; index++)
{
clone.CertificatePolicy.Add(policy.CertificatePolicy[index]);
}
}
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
clone.CustomTrustStore.AddRange(policy.CustomTrustStore);
#endif
clone.ExtraStore.AddRange(policy.ExtraStore);
return clone;
}
#endif
}
}