diff --git a/Snappier.Tests/SequenceHelpers.cs b/Snappier.Tests/SequenceHelpers.cs new file mode 100644 index 0000000..f8eae86 --- /dev/null +++ b/Snappier.Tests/SequenceHelpers.cs @@ -0,0 +1,45 @@ +using System; +using System.Buffers; + +#nullable enable + +namespace Snappier.Tests; + +public static class SequenceHelpers +{ + public static ReadOnlySequence CreateSequence(ReadOnlyMemory source, int maxSegmentSize) + { + ReadOnlySequenceSegment? lastSegment = null; + ReadOnlySequenceSegment? currentSegment = null; + + while (source.Length > 0) + { + int index = Math.Max(source.Length - maxSegmentSize, 0); + + currentSegment = new Segment( + source.Slice(index), + currentSegment, + index); + + lastSegment ??= currentSegment; + source = source.Slice(0, index); + } + + if (currentSegment is null) + { + return default; + } + + return new ReadOnlySequence(currentSegment, 0, lastSegment!, lastSegment!.Memory.Length); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, ReadOnlySequenceSegment? next, long runningIndex) + { + Memory = memory; + Next = next; + RunningIndex = runningIndex; + } + } +} diff --git a/Snappier.Tests/SnappyTests.cs b/Snappier.Tests/SnappyTests.cs index d59f8c1..dc14b8b 100644 --- a/Snappier.Tests/SnappyTests.cs +++ b/Snappier.Tests/SnappyTests.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Text; @@ -171,6 +172,53 @@ public void DecompressToMemory() Assert.True(input.AsSpan(0, bytesRead).SequenceEqual(output.Memory.Span)); } + [Fact] + public void DecompressToMemory_FromSequence() + { + using var resource = + typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.alice29.txt"); + Assert.NotNull(resource); + + var input = new byte[resource.Length]; + var bytesRead = resource.Read(input, 0, input.Length); + + var compressed = new byte[Snappy.GetMaxCompressedLength(bytesRead)]; + var compressedLength = Snappy.Compress(input.AsSpan(0, bytesRead), compressed); + + var compressedSequence = SequenceHelpers.CreateSequence(compressed.AsMemory(0, compressedLength), 1024); + + using var output = Snappy.DecompressToMemory(compressedSequence); + + Assert.Equal(bytesRead, output.Memory.Length); + Assert.True(input.AsSpan(0, bytesRead).SequenceEqual(output.Memory.Span)); + } + +#if NET6_0_OR_GREATER + + [Fact] + public void DecompressToBufferWriter_FromSequence() + { + using var resource = + typeof(SnappyTests).Assembly.GetManifestResourceStream($"Snappier.Tests.TestData.alice29.txt"); + Assert.NotNull(resource); + + var input = new byte[resource.Length]; + var bytesRead = resource.Read(input, 0, input.Length); + + var compressed = new byte[Snappy.GetMaxCompressedLength(bytesRead)]; + var compressedLength = Snappy.Compress(input.AsSpan(0, bytesRead), compressed); + + var compressedSequence = SequenceHelpers.CreateSequence(compressed.AsMemory(0, compressedLength), 1024); + + var writer = new ArrayBufferWriter(); + Snappy.Decompress(compressedSequence, writer); + + Assert.Equal(bytesRead, writer.WrittenCount); + Assert.True(input.AsSpan(0, bytesRead).SequenceEqual(writer.WrittenSpan)); + } + +#endif + [Fact] public void RandomData() { diff --git a/Snappier/Snappy.cs b/Snappier/Snappy.cs index 1b339be..eb51964 100644 --- a/Snappier/Snappy.cs +++ b/Snappier/Snappy.cs @@ -88,7 +88,7 @@ public static byte[] CompressToArray(ReadOnlySpan input) /// The length of the uncompressed data in the block. /// The data in has an invalid length. /// - /// This is useful for allocating a sufficient output buffer before calling . + /// This is useful for allocating a sufficient output buffer before calling . /// public static int GetUncompressedLength(ReadOnlySpan input) => SnappyDecompressor.ReadUncompressedLength(input); @@ -122,13 +122,27 @@ public static int Decompress(ReadOnlySpan input, Span output) return read; } + /// + /// Decompress a block of Snappy data. This must be an entire block. + /// + /// Data to decompress. + /// Buffer writer to receive the decompressed data. + /// Invalid Snappy block. + public static void Decompress(ReadOnlySequence input, IBufferWriter output) + { + using IMemoryOwner buffer = DecompressToMemory(input); + + output.Write(buffer.Memory.Span); + } + /// /// Decompress a block of Snappy to a new memory buffer. This must be an entire block. /// /// Data to decompress. /// An with the decompressed data. The caller is responsible for disposing this object. + /// Incomplete Snappy block. /// - /// Failing to dispose of the returned may result in memory leaks. + /// Failing to dispose of the returned may result in performance loss. /// public static IMemoryOwner DecompressToMemory(ReadOnlySpan input) { @@ -144,13 +158,40 @@ public static IMemoryOwner DecompressToMemory(ReadOnlySpan input) return decompressor.ExtractData(); } + /// + /// Decompress a block of Snappy to a new memory buffer. This must be an entire block. + /// + /// Data to decompress. + /// An with the decompressed data. The caller is responsible for disposing this object. + /// Incomplete Snappy block. + /// + /// Failing to dispose of the returned may result in performance loss. + /// + public static IMemoryOwner DecompressToMemory(ReadOnlySequence input) + { + using var decompressor = new SnappyDecompressor(); + + foreach (ReadOnlyMemory segment in input) + { + decompressor.Decompress(segment.Span); + } + + if (!decompressor.AllDataDecompressed) + { + ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block."); + } + + return decompressor.ExtractData(); + } + /// /// Decompress a block of Snappy to a new byte array. This must be an entire block. /// /// Data to decompress. /// The decompressed data. + /// Invalid Snappy block. /// - /// The resulting byte array is allocated on the heap. If possible, should + /// The resulting byte array is allocated on the heap. If possible, should /// be used instead since it uses a shared buffer pool. /// public static byte[] DecompressToArray(ReadOnlySpan input)