Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions Essentials.EncodingProviders.Base64/Base64EncodingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace ktsu.Essentials.EncodingProviders.Base64;
/// </summary>
/// <remarks>
/// The span paths use the UTF8 Base64 primitives directly, so they neither allocate an intermediate
/// string nor copy through a temporary array, except that encoding copies the source to a pooled buffer
/// first when it overlaps the destination.
/// string nor copy through a temporary array, except that encoding and decoding copy the source to a pooled
/// buffer first when it overlaps the destination.
/// </remarks>
public class Base64EncodingProvider : IEncodingProvider
{
Expand Down Expand Up @@ -110,6 +110,21 @@ public bool TryEncode(Stream data, Stream destination)
/// </remarks>
public bool TryDecode(ReadOnlySpan<byte> encodedData, Span<byte> destination, out int bytesWritten)
{
if (encodedData.Overlaps(destination))
{
// Base64.DecodeFromUtf8 does not support overlapping buffers, so decode from a copy.
byte[] copy = ArrayPool<byte>.Shared.Rent(encodedData.Length);
try
{
encodedData.CopyTo(copy);
return TryDecode(copy.AsSpan(0, encodedData.Length), destination, out bytesWritten);
}
finally
{
ArrayPool<byte>.Shared.Return(copy, clearArray: true);
}
}

#if !NET8_0_OR_GREATER
if (encodedData.IndexOfAny(Whitespace) >= 0)
{
Expand Down
19 changes: 17 additions & 2 deletions Essentials.EncodingProviders.Hex/HexEncodingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ namespace ktsu.Essentials.EncodingProviders.Hex;
/// </summary>
/// <remarks>
/// The span paths convert byte by byte, so they neither allocate an intermediate string nor copy
/// through a temporary array, except that encoding copies the source to a pooled buffer first when it
/// overlaps the destination. Encoding emits uppercase; decoding accepts either case.
/// through a temporary array, except that encoding and decoding copy the source to a pooled buffer first
/// when it overlaps the destination. Encoding emits uppercase; decoding accepts either case.
/// </remarks>
public class HexEncodingProvider : IEncodingProvider
{
Expand Down Expand Up @@ -124,6 +124,21 @@ public bool TryDecode(ReadOnlySpan<byte> encodedData, Span<byte> destination, ou
return false;
}

if (encodedData.Overlaps(destination))
{
// A destination starting after the source would overwrite pairs the loop has not read yet, so decode from a copy.
byte[] copy = ArrayPool<byte>.Shared.Rent(encodedData.Length);
try
{
encodedData.CopyTo(copy);
return TryDecode(copy.AsSpan(0, encodedData.Length), destination, out bytesWritten);
}
finally
{
ArrayPool<byte>.Shared.Return(copy, clearArray: true);
}
}

for (int i = 0; i < encodedData.Length; i += 2)
{
if (!TryParseNibble(encodedData[i], out int high) || !TryParseNibble(encodedData[i + 1], out int low))
Expand Down
22 changes: 22 additions & 0 deletions Essentials.Tests/EncodingProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@
}
}

[TestMethod]
[DynamicData(nameof(EncodingProviders))]
public void Decoding_OverlappingBuffers_MatchesOutOfPlace(IEncodingProvider encoder, string providerName)
{
byte[] original = Encoding.ASCII.GetBytes("0123456789ab");
byte[] encoded = encoder.Encode(original);
int maxLength = encoder.GetMaxDecodedLength(encoded.Length);

// A destination after the source used to overwrite encoded bytes before they were read (issue #166).
foreach ((int sourceOffset, int destinationOffset) in new[] { (0, 0), (0, 3), (3, 0) })
{
byte[] buffer = new byte[Math.Max(sourceOffset + encoded.Length, destinationOffset + maxLength)];
encoded.CopyTo(buffer, sourceOffset);

Assert.IsTrue(
encoder.TryDecode(buffer.AsSpan(sourceOffset, encoded.Length), buffer.AsSpan(destinationOffset, maxLength), out int written),
$"{providerName} should decode with source at {sourceOffset} and destination at {destinationOffset}");
CollectionAssert.AreEqual(original, buffer[destinationOffset..(destinationOffset + written)],
$"{providerName} should decode with source at {sourceOffset} and destination at {destinationOffset} as it does out of place");

Check warning on line 72 in Essentials.Tests/EncodingProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Essentials&issues=AaGKFWDK_dLe20mXxtch&open=AaGKFWDK_dLe20mXxtch&pullRequest=167
}
}

[TestMethod]
[DynamicData(nameof(EncodingProviders))]
public void GetMaxEncodedLength_Throws_When_The_Bound_Does_Not_Fit_An_Int(IEncodingProvider encoder, string providerName)
Expand Down
23 changes: 23 additions & 0 deletions Essentials.Tests/ObfuscationProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@
}
}

[TestMethod]
[DynamicData(nameof(ObfuscationProviders))]
public void Deobfuscation_OverlappingBuffers_MatchesOutOfPlace(IObfuscationProvider provider, string providerName)
{
byte[] original = Encoding.ASCII.GetBytes("0123456789ab");
byte[] obfuscated = provider.Obfuscate(original);
int maxLength = provider.GetMaxDeobfuscatedLength(obfuscated.Length);

// The decode direction of the test above: a destination after the source used to read bytes
// it had already overwritten (issue #166).
foreach ((int sourceOffset, int destinationOffset) in new[] { (0, 0), (0, 3), (3, 0) })
{
byte[] buffer = new byte[System.Math.Max(sourceOffset + obfuscated.Length, destinationOffset + maxLength)];
obfuscated.CopyTo(buffer, sourceOffset);

Assert.IsTrue(
provider.TryDeobfuscate(buffer.AsSpan(sourceOffset, obfuscated.Length), buffer.AsSpan(destinationOffset, maxLength), out int written),
$"{providerName} should deobfuscate with source at {sourceOffset} and destination at {destinationOffset}");
CollectionAssert.AreEqual(original, buffer[destinationOffset..(destinationOffset + written)],
$"{providerName} should deobfuscate with source at {sourceOffset} and destination at {destinationOffset} as it does out of place");

Check warning on line 163 in Essentials.Tests/ObfuscationProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Essentials&issues=AaGKFWIh_dLe20mXxtci&open=AaGKFWIh_dLe20mXxtci&pullRequest=167
}
}

[TestMethod]
public void Obfuscation_Base64_Roundtrip_String()
{
Expand Down
Loading