diff --git a/Snappier.Benchmarks/BlockCompressHtml.cs b/Snappier.Benchmarks/BlockCompressHtml.cs
index ea7bcb8..17dd9c6 100644
--- a/Snappier.Benchmarks/BlockCompressHtml.cs
+++ b/Snappier.Benchmarks/BlockCompressHtml.cs
@@ -27,7 +27,9 @@ public int Compress()
{
using var compressor = new SnappyCompressor();
+#pragma warning disable CS0618 // Type or member is obsolete
return compressor.Compress(_input.Span, _output.Span);
+#pragma warning restore CS0618 // Type or member is obsolete
}
}
}
diff --git a/Snappier.Benchmarks/Overview.cs b/Snappier.Benchmarks/Overview.cs
index 01c2667..2122c8b 100644
--- a/Snappier.Benchmarks/Overview.cs
+++ b/Snappier.Benchmarks/Overview.cs
@@ -53,7 +53,9 @@ public int BlockCompress64KbHtml()
{
using var compressor = new SnappyCompressor();
+#pragma warning disable CS0618 // Type or member is obsolete
return compressor.Compress(_htmlMemory.Span, _outputBuffer.Span);
+#pragma warning restore CS0618 // Type or member is obsolete
}
[Benchmark]
diff --git a/Snappier.Tests/SnappyTests.cs b/Snappier.Tests/SnappyTests.cs
index ab6526b..8db6a3a 100644
--- a/Snappier.Tests/SnappyTests.cs
+++ b/Snappier.Tests/SnappyTests.cs
@@ -67,6 +67,60 @@ public void CompressAndDecompressFile_LimitedOutputBuffer()
Assert.Equal(input, output);
}
+ [Fact]
+ public void Compress_InsufficientOutputBuffer()
+ {
+ using var resource =
+ typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.alice29.txt");
+ Assert.NotNull(resource);
+
+ var input = new byte[65536];
+ var bytesRead = resource.Read(input, 0, input.Length);
+
+ var compressed = new byte[1024];
+ Assert.Throws(() => Snappy.Compress(input.AsSpan(0, bytesRead), compressed));
+ }
+
+ [Fact]
+ public void TryCompressAndDecompress()
+ {
+ using var resource =
+ typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.alice29.txt");
+ Assert.NotNull(resource);
+
+ var input = new byte[65536];
+ var bytesRead = resource.Read(input, 0, input.Length);
+
+ var compressed = new byte[Snappy.GetMaxCompressedLength(bytesRead)];
+ var result = Snappy.TryCompress(input.AsSpan(0, bytesRead), compressed, out var compressedLength);
+ Assert.True(result);
+
+ var compressedSpan = compressed.AsSpan(0, compressedLength);
+
+ var output = new byte[Snappy.GetUncompressedLength(compressedSpan)];
+ result = Snappy.TryDecompress(compressedSpan, output, out var outputLength);
+ Assert.True(result);
+
+ Assert.Equal(input.Length, outputLength);
+ Assert.Equal(input, output);
+ }
+
+ [Fact]
+ public void TryCompress_InsufficientOutputBuffer()
+ {
+ using var resource =
+ typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.alice29.txt");
+ Assert.NotNull(resource);
+
+ var input = new byte[65536];
+ var bytesRead = resource.Read(input, 0, input.Length);
+
+ var compressed = new byte[1024];
+ var result = Snappy.TryCompress(input.AsSpan(0, bytesRead), compressed, out _);
+
+ Assert.False(result);
+ }
+
#if NET6_0_OR_GREATER
[Theory]
@@ -167,6 +221,21 @@ public void BadData_InsufficentOutputBuffer_ThrowsArgumentException()
});
}
+ [Fact]
+ public void TryDecompress_InsufficentOutputBuffer_ReturnsFalse()
+ {
+ var input = new byte[100000];
+ ArrayFill(input, (byte)'A');
+
+ var compressed = new byte[Snappy.GetMaxCompressedLength(input.Length)];
+ var compressedLength = Snappy.Compress(input, compressed);
+
+ var output = new byte[100];
+ var result = Snappy.TryDecompress(compressed.AsSpan(0, compressedLength), output, out _);
+
+ Assert.False(result);
+ }
+
[Fact]
public void BadData_SimpleCorruption_ThrowsInvalidDataException()
{
@@ -234,6 +303,29 @@ public void BadData_FromFile_ThrowsInvalidDataException(string filename)
});
}
+ [Theory]
+ [InlineData("baddata1.snappy")]
+ [InlineData("baddata2.snappy")]
+ [InlineData("baddata3.snappy")]
+ public void BadData_TryDecompress_ThrowsInvalidDataException(string filename)
+ {
+ using var resource =
+ typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.{filename}");
+ Assert.NotNull(resource);
+
+ var input = new byte[resource.Length];
+ var bytesRead = resource.Read(input, 0, input.Length);
+
+ Assert.Throws(() =>
+ {
+ var length = Snappy.GetUncompressedLength(input.AsSpan(0, bytesRead));
+ Assert.InRange(length, 0, 1 << 20);
+
+ var output = new byte[length];
+ Snappy.TryDecompress(input.AsSpan(0, bytesRead), output, out _);
+ });
+ }
+
[Fact]
public void DecompressToMemory()
{
diff --git a/Snappier/Internal/SnappyCompressor.cs b/Snappier/Internal/SnappyCompressor.cs
index 7e7fa75..530493f 100644
--- a/Snappier/Internal/SnappyCompressor.cs
+++ b/Snappier/Internal/SnappyCompressor.cs
@@ -9,7 +9,18 @@ internal class SnappyCompressor : IDisposable
{
private HashTable? _workingMemory = new();
+ [Obsolete("Retained for benchmark comparisons to previous versions")]
public int Compress(ReadOnlySpan input, Span output)
+ {
+ if (!TryCompress(input, output, out int bytesWritten))
+ {
+ ThrowHelper.ThrowArgumentExceptionInsufficientOutputBuffer(nameof(output));
+ }
+
+ return bytesWritten;
+ }
+
+ public bool TryCompress(ReadOnlySpan input, Span output, out int bytesWritten)
{
if (_workingMemory == null)
{
@@ -18,7 +29,7 @@ public int Compress(ReadOnlySpan input, Span output)
_workingMemory.EnsureCapacity(input.Length);
- int bytesWritten = VarIntEncoding.Write(output, (uint)input.Length);
+ bytesWritten = VarIntEncoding.Write(output, (uint)input.Length);
output = output.Slice(bytesWritten);
while (input.Length > 0)
@@ -29,15 +40,13 @@ public int Compress(ReadOnlySpan input, Span output)
int maxOutput = Helpers.MaxCompressedLength(fragment.Length);
+ int written;
if (output.Length >= maxOutput)
{
// The output span is large enough to hold the maximum possible compressed output,
// compress directly to that span.
- int written = CompressFragment(fragment, output, hashTable);
-
- output = output.Slice(written);
- bytesWritten += written;
+ written = CompressFragment(fragment, output, hashTable);
}
else
{
@@ -47,15 +56,14 @@ public int Compress(ReadOnlySpan input, Span output)
byte[] scratch = ArrayPool.Shared.Rent(maxOutput);
try
{
- int written = CompressFragment(fragment, scratch.AsSpan(), hashTable);
+ written = CompressFragment(fragment, scratch.AsSpan(), hashTable);
if (output.Length < written)
{
- ThrowHelper.ThrowArgumentException("Insufficient output buffer", nameof(output));
+ bytesWritten = 0;
+ return false;
}
scratch.AsSpan(0, written).CopyTo(output);
- output = output.Slice(written);
- bytesWritten += written;
}
finally
{
@@ -63,10 +71,13 @@ public int Compress(ReadOnlySpan input, Span output)
}
}
+ output = output.Slice(written);
+ bytesWritten += written;
+
input = input.Slice(fragment.Length);
}
- return bytesWritten;
+ return true;
}
public void Compress(ReadOnlySequence input, IBufferWriter bufferWriter)
diff --git a/Snappier/Internal/SnappyStreamCompressor.cs b/Snappier/Internal/SnappyStreamCompressor.cs
index 6ebb1c5..2e46fce 100644
--- a/Snappier/Internal/SnappyStreamCompressor.cs
+++ b/Snappier/Internal/SnappyStreamCompressor.cs
@@ -214,7 +214,11 @@ private void CompressBlock(ReadOnlySpan input)
// Make room for the header and CRC
Span blockBody = output.Slice(headerSize);
- int bytesWritten = _compressor.Compress(input, blockBody);
+ if (!_compressor.TryCompress(input, blockBody, out int bytesWritten))
+ {
+ // Should be unreachable since we're allocating a buffer of the correct size.
+ ThrowHelper.ThrowInvalidOperationException();
+ }
if (bytesWritten < input.Length)
{
diff --git a/Snappier/Internal/ThrowHelper.cs b/Snappier/Internal/ThrowHelper.cs
index b16cc84..19d85e6 100644
--- a/Snappier/Internal/ThrowHelper.cs
+++ b/Snappier/Internal/ThrowHelper.cs
@@ -32,12 +32,22 @@ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpres
}
#endif
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)] // Avoid inlining to reduce code size for a cold path
+ public static void ThrowArgumentExceptionInsufficientOutputBuffer(string? paramName) =>
+ ThrowArgumentException("Output buffer is too small.", paramName);
+
[DoesNotReturn]
public static void ThrowInvalidDataException(string? message) =>
throw new InvalidDataException(message);
[DoesNotReturn]
- public static void ThrowInvalidOperationException(string? message) =>
+ [MethodImpl(MethodImplOptions.NoInlining)] // Avoid inlining to reduce code size for a cold path
+ public static void ThrowInvalidDataExceptionIncompleteSnappyBlock() =>
+ throw new InvalidDataException("Incomplete Snappy block.");
+
+ [DoesNotReturn]
+ public static void ThrowInvalidOperationException(string? message = null) =>
throw new InvalidOperationException(message);
[DoesNotReturn]
diff --git a/Snappier/Snappy.cs b/Snappier/Snappy.cs
index 8d2bbac..1b2e15a 100644
--- a/Snappier/Snappy.cs
+++ b/Snappier/Snappy.cs
@@ -31,14 +31,39 @@ public static int GetMaxCompressedLength(int inputLength) =>
/// Data to compress.
/// Buffer to receive the compressed data.
/// Number of bytes written to .
+ /// Output buffer is too small.
///
/// The output buffer must be large enough to contain the compressed output.
///
public static int Compress(ReadOnlySpan input, Span output)
{
+ if (!TryCompress(input, output, out var bytesWritten))
+ {
+ ThrowHelper.ThrowArgumentExceptionInsufficientOutputBuffer(nameof(output));
+ }
+
+ return bytesWritten;
+ }
+
+ ///
+ /// Attempt to compress the input data into the output buffer.
+ ///
+ /// Data to compress.
+ /// Buffer to receive the compressed data.
+ /// Number of bytes written to the .
+ /// true if the compression was successful, false if the output buffer is too small.
+ public static bool TryCompress(ReadOnlySpan input, Span output, out int bytesWritten)
+ {
+ if (output.IsEmpty)
+ {
+ // Minimum of 1 byte is required to store a zero-length block, short circuit.
+ bytesWritten = 0;
+ return false;
+ }
+
using var compressor = new SnappyCompressor();
- return compressor.Compress(input, output);
+ return compressor.TryCompress(input, output, out bytesWritten);
}
///
@@ -46,9 +71,13 @@ public static int Compress(ReadOnlySpan input, Span output)
///
/// Data to compress.
/// Buffer writer to receive the compressed data.
+ /// is null.
+ /// is larger than the maximum of 4,294,967,295 bytes.
///
- /// For the best performance the input sequence should be comprised of segments some multiple of 64KB
- /// in size or a single wrapped in a sequence.
+ ///
+ /// For the best performance, sequences with more than one segement should be comprised of segments some multiple of 64KB
+ /// in size (i.e. 64KB or 128KB or 256KB each) with only the final segment varying.
+ ///
///
public static void Compress(ReadOnlySequence input, IBufferWriter output)
{
@@ -73,7 +102,11 @@ public static IMemoryOwner CompressToMemory(ReadOnlySpan input)
try
{
- int length = Compress(input, buffer);
+ if (!TryCompress(input, buffer, out int length))
+ {
+ // Should be unreachable since we're allocating a buffer of the correct size.
+ ThrowHelper.ThrowInvalidOperationException();
+ }
return new ByteArrayPoolMemoryOwner(buffer, length);
}
@@ -123,6 +156,25 @@ public static int GetUncompressedLength(ReadOnlySpan input) =>
/// Invalid Snappy block.
/// Output buffer is too small.
public static int Decompress(ReadOnlySpan input, Span output)
+ {
+ bool result = TryDecompress(input, output, out int bytesWritten);
+ if (!result)
+ {
+ ThrowHelper.ThrowArgumentExceptionInsufficientOutputBuffer(nameof(output));
+ }
+
+ return bytesWritten;
+ }
+
+ ///
+ /// Decompress a block of Snappy data. This must be an entire block.
+ ///
+ /// Data to decompress.
+ /// Buffer to receive the decompressed data.
+ /// Number of bytes written to the .
+ /// true if the compression was successful, false if the output buffer is too small.
+ /// Invalid Snappy block.
+ public static bool TryDecompress(ReadOnlySpan input, Span output, out int bytesWritten)
{
using var decompressor = new SnappyDecompressor();
@@ -130,17 +182,12 @@ public static int Decompress(ReadOnlySpan input, Span output)
if (!decompressor.AllDataDecompressed)
{
- ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block.");
+ ThrowHelper.ThrowInvalidDataExceptionIncompleteSnappyBlock();
}
- int read = decompressor.Read(output);
-
- if (!decompressor.EndOfFile)
- {
- ThrowHelper.ThrowArgumentException("Output buffer is too small.", nameof(output));
- }
+ bytesWritten = decompressor.Read(output);
- return read;
+ return decompressor.EndOfFile;
}
///
@@ -165,7 +212,7 @@ public static void Decompress(ReadOnlySequence input, IBufferWriter
if (!decompressor.AllDataDecompressed)
{
- ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block.");
+ ThrowHelper.ThrowInvalidDataExceptionIncompleteSnappyBlock();
}
}
@@ -186,7 +233,7 @@ public static IMemoryOwner DecompressToMemory(ReadOnlySpan input)
if (!decompressor.AllDataDecompressed)
{
- ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block.");
+ ThrowHelper.ThrowInvalidDataExceptionIncompleteSnappyBlock();
}
return decompressor.ExtractData();
@@ -212,7 +259,7 @@ public static IMemoryOwner DecompressToMemory(ReadOnlySequence input
if (!decompressor.AllDataDecompressed)
{
- ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block.");
+ ThrowHelper.ThrowInvalidDataExceptionIncompleteSnappyBlock();
}
return decompressor.ExtractData();