Skip to content

Commit

Permalink
Correct redaction of Array.Empty<char>() and new char[0] (#4309)
Browse files Browse the repository at this point in the history
Fixes #4308
  • Loading branch information
RussKie authored Aug 28, 2023
1 parent 03d4f61 commit c7f3469
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public string Redact(ReadOnlySpan<char> source)
return string.Empty;
}

var length = GetRedactedLength(source);
int length = GetRedactedLength(source);

#if NETCOREAPP3_1_OR_GREATER
unsafe
Expand Down Expand Up @@ -115,12 +115,12 @@ public string Redact<T>(T value, string? format = null, IFormatProvider? provide

// 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 var written, format.AsSpan(), provider))
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);
var length = GetRedactedLength(formatted);
int length = GetRedactedLength(formatted);

unsafe
{
Expand All @@ -140,6 +140,16 @@ public string Redact<T>(T value, string? format = null, IFormatProvider? provide
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());
}

Expand All @@ -166,7 +176,7 @@ public int Redact<T>(T value, Span<char> destination, string? format = null, IFo
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 var written, format.AsSpan(), provider))
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);
Expand All @@ -181,6 +191,16 @@ public int Redact<T>(T value, Span<char> destination, string? format = null, IFo
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);
}

Expand All @@ -207,12 +227,12 @@ public bool TryRedact<T>(T value, Span<char> destination, out int charsWritten,
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 var written, format, provider))
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);

var rlen = GetRedactedLength(formatted);
int rlen = GetRedactedLength(formatted);
if (rlen > destination.Length)
{
charsWritten = 0;
Expand All @@ -225,25 +245,40 @@ public bool TryRedact<T>(T value, Span<char> destination, out int charsWritten,
}
#endif

string? str;
string? str = null;
ReadOnlySpan<char> ros = default;
if (value is IFormattable)
{
var fmt = format.Length > 0 ? format.ToString() : string.Empty;
str = ((IFormattable)value).ToString(fmt, provider);
}
else 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.
//
// 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
{
str = value?.ToString();
}

var len = GetRedactedLength(str);
if (str is not null)
{
ros = str.AsSpan();
}

int len = GetRedactedLength(ros);
if (len > destination.Length)
{
charsWritten = 0;
return false;
}

charsWritten = Redact(str, destination);
charsWritten = Redact(ros, destination);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static void User_Can_Get_String_From_IRedactor_Using_Extension_Method_Wit

Redactor r = NullRedactor.Instance;

var redacted = r.Redact(data);
string redacted = r.Redact(data);

Assert.Equal(data, redacted);
}
Expand All @@ -46,8 +46,8 @@ public static void Get_Redacted_String_API_Returns_Equivalent_Output_As_Span_Ove
var data = new string('3', 3);
var r = NullRedactor.Instance;

var lengthFromExtension = r.GetRedactedLength(data);
var length = r.GetRedactedLength(data);
int lengthFromExtension = r.GetRedactedLength(data);
int length = r.GetRedactedLength(data);

Assert.Equal(lengthFromExtension, length);
}
Expand All @@ -61,8 +61,8 @@ public static void Redact_Extension_String_Span_Works_The_Same_Way_As_Native_Met
Span<char> buffer = stackalloc char[3];

var r = new PassthroughRedactor();
var extensionWritten = r.Redact(data, extBuffer);
var written = r.Redact(data, buffer);
int extensionWritten = r.Redact(data, extBuffer);
int written = r.Redact(data, buffer);

Assert.Equal(extensionWritten, written);
Assert.Equal(extBuffer.ToString(), buffer.ToString());
Expand All @@ -82,8 +82,8 @@ public static void SpanFormattable_Format_And_Redacts_Data(int inputSize)

var r = new PassthroughRedactor();

var redacted = r.Redact(spanFormattable, null, null);
var redactedDirectly = r.Redact(data);
string redacted = r.Redact(spanFormattable, null, null);
string redactedDirectly = r.Redact(data);

Assert.Equal(redactedDirectly, redacted);
}
Expand All @@ -102,10 +102,10 @@ public static void SpanFormattable_Format_And_Redacts_Data_With_Destination_Buff
var buffer = new char[data.Length];
var bufferDirect = new char[data.Length];

var redacted = r.Redact(spanFormattable, buffer, null, null);
var redactedDirectly = r.Redact(data, bufferDirect);
int redacted = r.Redact(spanFormattable, buffer, null, null);
int redactedDirectly = r.Redact(data, bufferDirect);

for (var i = 0; i < buffer.Length; i++)
for (int i = 0; i < buffer.Length; i++)
{
Assert.Equal(buffer[i], bufferDirect[i]);
}
Expand All @@ -115,14 +115,14 @@ public static void SpanFormattable_Format_And_Redacts_Data_With_Destination_Buff
[Fact]
public static void Formattable_Format_And_Redacts_Data()
{
var data = Guid.NewGuid().ToString();
string data = Guid.NewGuid().ToString();

var formattable = new TestFormattable(data);

var r = new PassthroughRedactor();

var redacted = r.Redact(formattable, null, null);
var redactedDirectly = r.Redact(data);
string redacted = r.Redact(formattable, null, null);
string redactedDirectly = r.Redact(data);

Assert.Equal(redactedDirectly, redacted);
}
Expand All @@ -139,10 +139,10 @@ public static void Formattable_Format_And_Redacts_Data_With_Destination_Buffer()
var buffer = new char[data.Length];
var bufferDirect = new char[data.Length];

var redacted = r.Redact(spanFormattable, buffer, null, null);
var redactedDirectly = r.Redact(data, bufferDirect);
int redacted = r.Redact(spanFormattable, buffer, null, null);
int redactedDirectly = r.Redact(data, bufferDirect);

for (var i = 0; i < buffer.Length; i++)
for (int i = 0; i < buffer.Length; i++)
{
Assert.Equal(buffer[i], bufferDirect[i]);
}
Expand All @@ -160,8 +160,8 @@ public static void Object_Format_And_Redacts_Data()
var buffer = new char[data.Length];
var bufferDirect = new char[data.Length];

var redacted = r.Redact(obj);
var redactedDirectly = r.Redact(data);
string redacted = r.Redact(obj);
string redactedDirectly = r.Redact(data);

Assert.Equal(redactedDirectly, redacted);
}
Expand All @@ -178,15 +178,65 @@ public static void Object_Format_And_Redacts_Data_With_Destination_Buffer()
var buffer = new char[data.Length];
var bufferDirect = new char[data.Length];

var redacted = r.Redact(obj, buffer);
var redactedDirectly = r.Redact(data, bufferDirect);
int redacted = r.Redact(obj, buffer);
int redactedDirectly = r.Redact(data, bufferDirect);

for (var i = 0; i < buffer.Length; i++)
for (int i = 0; i < buffer.Length; i++)
{
Assert.Equal(buffer[i], bufferDirect[i]);
}
}

[Fact]
public static void ArrayEmptyOfChar_Redacted_correctly()
{
var r = new PassthroughRedactor();
string redacted = r.Redact(Array.Empty<char>());

Assert.Equal("", redacted);
}

[Fact]
public static void ArrayOfChar_Redacted_correctly()
{
var r = new PassthroughRedactor();
string redacted = r.Redact(new char[0]);

Assert.Equal("", redacted);
}

[Fact]
public static void ArrayEmptyOfChar_With_Destination_Buffer_Redacted_correctly()
{
char[] buffer = new char[5];

var r = new PassthroughRedactor();
int written = r.Redact(Array.Empty<char>(), buffer);

Assert.Equal(0, written);

foreach (char item in buffer)
{
Assert.Equal('\0', item);
}
}

[Fact]
public static void ArrayOfChar_With_Destination_Buffer_Redacted_correctly()
{
char[] buffer = new char[5];

var r = new PassthroughRedactor();
int written = r.Redact(new char[0], buffer);

Assert.Equal(0, written);

foreach (char item in buffer)
{
Assert.Equal('\0', item);
}
}

[Theory]
[InlineData(35, false)]
[InlineData(36, true)]
Expand Down Expand Up @@ -241,6 +291,38 @@ public static void TryRedact_BufferSizes_CustomFormat(int bufferSize, bool succe
}
}

[Fact]
public static void TryRedact_ArrayEmptyOfChar_With_Destination_Buffer_Redacted_correctly()
{
char[] buffer = new char[5];

var r = new PassthroughRedactor();
Assert.True(r.TryRedact(Array.Empty<char>(), buffer, out int charsWritten, string.Empty.AsSpan(), null));

Assert.Equal(0, charsWritten);

foreach (char item in buffer)
{
Assert.Equal('\0', item);
}
}

[Fact]
public static void TryRedact_ArrayOfChar_With_Destination_Buffer_Redacted_correctly()
{
char[] buffer = new char[5];

var r = new PassthroughRedactor();
Assert.True(r.TryRedact(new char[0], buffer, out int charsWritten, string.Empty.AsSpan(), null));

Assert.Equal(0, charsWritten);

foreach (char item in buffer)
{
Assert.Equal('\0', item);
}
}

private class NonFormatable
{
public override string ToString() => "123456789012345678901234567890123456";
Expand Down

0 comments on commit c7f3469

Please sign in to comment.