File: Redaction\Redactor.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Compliance.Abstractions\Microsoft.Extensions.Compliance.Abstractions.csproj (Microsoft.Extensions.Compliance.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
 
#if !NETCOREAPP3_1_OR_GREATER
using System.Buffers;
#endif
 
namespace Microsoft.Extensions.Compliance.Redaction;
 
/// <summary>
/// Enables the redaction of potentially sensitive data.
/// </summary>
public abstract class Redactor
{
#if NET6_0_OR_GREATER
    private const int MaximumStackAllocation = 256;
#endif
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <param name="source">Value to redact.</param>
    /// <returns>Redacted value.</returns>
    public string Redact(ReadOnlySpan<char> source)
    {
        if (source.IsEmpty)
        {
            return string.Empty;
        }
 
        int length = GetRedactedLength(source);
 
#if NETCOREAPP3_1_OR_GREATER
        unsafe
        {
#pragma warning disable 8500
            return string.Create(
                length,
                (this, (IntPtr)(&source)),
                static (destination, state) => state.Item1.Redact(*(ReadOnlySpan<char>*)state.Item2, destination));
#pragma warning restore 8500
        }
#else
        var buffer = ArrayPool<char>.Shared.Rent(length);
 
        try
        {
            var charsWritten = Redact(source, buffer);
            var redactedString = new string(buffer, 0, charsWritten);
 
            return redactedString;
        }
        finally
        {
            ArrayPool<char>.Shared.Return(buffer);
        }
#endif
    }
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <param name="source">Value to redact.</param>
    /// <param name="destination">Buffer to store redacted value.</param>
    /// <returns>Number of characters produced when redacting the given source input.</returns>
    /// <exception cref="ArgumentException"><paramref name="destination"/> is too small.</exception>
    public abstract int Redact(ReadOnlySpan<char> source, Span<char> destination);
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <param name="source">Value to redact.</param>
    /// <param name="destination">Buffer to redact into.</param>
    /// <remarks>
    /// Returns 0 when <paramref name="source"/> is <see langword="null"/>.
    /// </remarks>
    /// <returns>Number of characters written to the buffer.</returns>
    /// <exception cref="ArgumentException"><paramref name="destination"/> is too small.</exception>
    public int Redact(string? source, Span<char> destination) => Redact(source.AsSpan(), destination);
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <param name="source">Value to redact.</param>
    /// <returns>Redacted value.</returns>
    /// <remarks>
    /// Returns an empty string when <paramref name="source"/> is <see langword="null"/>.
    /// </remarks>
    /// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
    public virtual string Redact(string? source) => Redact(source.AsSpan());
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <typeparam name="T">Type of value to redact.</typeparam>
    /// <param name="value">Value to redact.</param>
    /// <param name="format">
    /// The optional format that selects the specific formatting operation performed. Refer to the
    /// documentation of the type being formatted to understand the values you can supply here.
    /// </param>
    /// <param name="provider">Format provider used to produce a string representing the value.</param>
    /// <returns>Redacted value.</returns>
    [SkipLocalsInit]
    [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")]
    public string Redact<T>(T value, string? format = null, IFormatProvider? provider = null)
    {
#if NET6_0_OR_GREATER
        if (value is ISpanFormattable)
        {
            Span<char> buffer = stackalloc char[MaximumStackAllocation];
 
            // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
            // Null forgiving operator: The null case is checked with default equality comparer, but compiler doesn't understand it.
            if (((ISpanFormattable)value).TryFormat(buffer, out int written, format.AsSpan(), provider))
            {
                // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
 
                var formatted = buffer.Slice(0, written);
                int length = GetRedactedLength(formatted);
 
                unsafe
                {
#pragma warning disable 8500
                    return string.Create(
                        length,
                        (this, (IntPtr)(&formatted)),
                        static (destination, state) => state.Item1.Redact(*(ReadOnlySpan<char>*)state.Item2, destination));
#pragma warning restore 8500
                }
            }
        }
#endif
 
        if (value is IFormattable)
        {
            return Redact(((IFormattable)value).ToString(format, provider));
        }
 
        if (value is char[])
        {
            // An attempt to call value.ToString() on a char[] will produce a string "System.Char[]" and all redaction will be attempted on it,
            // instead of the provided array. This will lead to incorrectly allocated buffers.
            //
            // NB: not using pattern matching as it is recognized by the JIT and since this is a generic type, the JIT ends up generating code
            // without any of those conditional statements being present. But this only happens when not using pattern matching.
            return Redact(((char[])(object)value).AsSpan());
        }
 
        return Redact(value?.ToString());
    }
 
    /// <summary>
    /// Redacts potentially sensitive data.
    /// </summary>
    /// <typeparam name="T">Type of value to redact.</typeparam>
    /// <param name="value">Value to redact.</param>
    /// <param name="destination">Buffer to redact into.</param>
    /// <param name="format">
    /// The optional format string that selects the specific formatting operation performed. Refer to the
    /// documentation of the type being formatted to understand the values you can supply here.
    /// </param>
    /// <param name="provider">Format provider used to produce a string representing the value.</param>
    /// <returns>Number of characters written to the buffer.</returns>
    /// <exception cref="ArgumentException"><paramref name="destination"/> is too small.</exception>
    [SkipLocalsInit]
    [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")]
    public int Redact<T>(T value, Span<char> destination, string? format = null, IFormatProvider? provider = null)
    {
#if NET6_0_OR_GREATER
        if (value is ISpanFormattable)
        {
            Span<char> buffer = stackalloc char[MaximumStackAllocation];
 
            // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
            if (((ISpanFormattable)value).TryFormat(buffer, out int written, format.AsSpan(), provider))
            {
                // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
                var formatted = buffer.Slice(0, written);
 
                return Redact(formatted, destination);
            }
        }
#endif
 
        if (value is IFormattable)
        {
            return Redact(((IFormattable)value).ToString(format, provider), destination);
        }
 
        if (value is char[])
        {
            // An attempt to call value.ToString() on a char[] will produce a string "System.Char[]" and all redaction will be attempted on it,
            // instead of the provided array. This will lead to incorrectly allocated buffers.
            //
            // NB: not using pattern matching as it is recognized by the JIT and since this is a generic type, the JIT ends up generating code
            // without any of those conditional statements being present. But this only happens when not using pattern matching.
            return Redact(((char[])(object)value).AsSpan(), destination);
        }
 
        return Redact(value?.ToString(), destination);
    }
 
    /// <summary>
    /// Tries to redact potentially sensitive data.
    /// </summary>
    /// <typeparam name="T">The type of value to redact.</typeparam>
    /// <param name="value">The value to redact.</param>
    /// <param name="destination">The buffer to redact into.</param>
    /// <param name="charsWritten">When this method returns, contains the number of redacted characters that were written to the destination buffer.</param>
    /// <param name="format">
    /// The format string that selects the specific formatting operation performed. Refer to the
    /// documentation of the type being formatted to understand the values you can supply here.
    /// </param>
    /// <param name="provider">The format provider used to produce a string representing the value.</param>
    /// <returns><see langword="true"/> if the destination buffer was large enough, otherwise <see langword="false"/>.</returns>
    [SkipLocalsInit]
    [SuppressMessage("Minor Code Smell", "S3247:Duplicate casts should not be made", Justification = "Avoid pattern matching to improve jitted code")]
    public bool TryRedact<T>(T value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider = null)
    {
#if NET6_0_OR_GREATER
        if (value is ISpanFormattable)
        {
            Span<char> buffer = stackalloc char[MaximumStackAllocation];
 
            // Stryker disable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
            if (((ISpanFormattable)value).TryFormat(buffer, out int written, format, provider))
            {
                // Stryker enable all : Cannot kill the mutant because the only difference is allocating buffer on stack or renting it.
                var formatted = buffer.Slice(0, written);
 
                int rlen = GetRedactedLength(formatted);
                if (rlen > destination.Length)
                {
                    charsWritten = 0;
                    return false;
                }
 
                charsWritten = Redact(formatted, destination);
                return true;
            }
        }
#endif
 
        ReadOnlySpan<char> ros = default;
        if (value is IFormattable)
        {
            var fmt = format.Length > 0 ? format.ToString() : string.Empty;
            var str = ((IFormattable)value).ToString(fmt, provider);
            if (str != null)
            {
                ros = str.AsSpan();
            }
        }
        else if (value is char[])
        {
            // An attempt to call value.ToString() on a char[] will produce the string "System.Char[]" and redaction will be attempted on it,
            // instead of the provided array.
            //
            // Not using pattern matching as it is recognized by the JIT and since this is a generic type, the JIT ends up generating code
            // without any of those conditional statements being present. But this only happens when not using pattern matching.
            ros = ((char[])(object)value).AsSpan();
        }
        else
        {
            var str = value?.ToString();
            if (str != null)
            {
                ros = str.AsSpan();
            }
        }
 
        int len = GetRedactedLength(ros);
        if (len > destination.Length)
        {
            charsWritten = 0;
            return false;
        }
 
        charsWritten = Redact(ros, destination);
        return true;
    }
 
    /// <summary>
    /// Gets the number of characters produced by redacting the input.
    /// </summary>
    /// <param name="input">Value to be redacted.</param>
    /// <returns>The number of characters produced by redacting the input.</returns>
    public abstract int GetRedactedLength(ReadOnlySpan<char> input);
 
    /// <summary>
    /// Gets the number of characters produced by redacting the input.
    /// </summary>
    /// <param name="input">Value to be redacted.</param>
    /// <returns>Minimum buffer size.</returns>
    public int GetRedactedLength(string? input) => GetRedactedLength(input.AsSpan());
}