Skip to content

Commit

Permalink
Add overloads to decompress from ReadOnlySequence<byte> (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
brantburnett authored Dec 1, 2024
1 parent 712659b commit 5a01bad
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 3 deletions.
45 changes: 45 additions & 0 deletions Snappier.Tests/SequenceHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Buffers;

#nullable enable

namespace Snappier.Tests;

public static class SequenceHelpers
{
public static ReadOnlySequence<byte> CreateSequence(ReadOnlyMemory<byte> source, int maxSegmentSize)
{
ReadOnlySequenceSegment<byte>? lastSegment = null;
ReadOnlySequenceSegment<byte>? 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<byte>(currentSegment, 0, lastSegment!, lastSegment!.Memory.Length);
}

private sealed class Segment : ReadOnlySequenceSegment<byte>
{
public Segment(ReadOnlyMemory<byte> memory, ReadOnlySequenceSegment<byte>? next, long runningIndex)
{
Memory = memory;
Next = next;
RunningIndex = runningIndex;
}
}
}
48 changes: 48 additions & 0 deletions Snappier.Tests/SnappyTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -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<byte>();
Snappy.Decompress(compressedSequence, writer);

Assert.Equal(bytesRead, writer.WrittenCount);
Assert.True(input.AsSpan(0, bytesRead).SequenceEqual(writer.WrittenSpan));
}

#endif

[Fact]
public void RandomData()
{
Expand Down
47 changes: 44 additions & 3 deletions Snappier/Snappy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public static byte[] CompressToArray(ReadOnlySpan<byte> input)
/// <returns>The length of the uncompressed data in the block.</returns>
/// <exception cref="InvalidDataException">The data in <paramref name="input"/> has an invalid length.</exception>
/// <remarks>
/// This is useful for allocating a sufficient output buffer before calling <see cref="Decompress"/>.
/// This is useful for allocating a sufficient output buffer before calling <see cref="Decompress(ReadOnlySpan{byte}, Span{byte})"/>.
/// </remarks>
public static int GetUncompressedLength(ReadOnlySpan<byte> input) =>
SnappyDecompressor.ReadUncompressedLength(input);
Expand Down Expand Up @@ -122,13 +122,27 @@ public static int Decompress(ReadOnlySpan<byte> input, Span<byte> output)
return read;
}

/// <summary>
/// Decompress a block of Snappy data. This must be an entire block.
/// </summary>
/// <param name="input">Data to decompress.</param>
/// <param name="output">Buffer writer to receive the decompressed data.</param>
/// <exception cref="InvalidDataException">Invalid Snappy block.</exception>
public static void Decompress(ReadOnlySequence<byte> input, IBufferWriter<byte> output)
{
using IMemoryOwner<byte> buffer = DecompressToMemory(input);

output.Write(buffer.Memory.Span);
}

/// <summary>
/// Decompress a block of Snappy to a new memory buffer. This must be an entire block.
/// </summary>
/// <param name="input">Data to decompress.</param>
/// <returns>An <see cref="IMemoryOwner{T}"/> with the decompressed data. The caller is responsible for disposing this object.</returns>
/// <exception cref="InvalidDataException">Incomplete Snappy block.</exception>
/// <remarks>
/// Failing to dispose of the returned <see cref="IMemoryOwner{T}"/> may result in memory leaks.
/// Failing to dispose of the returned <see cref="IMemoryOwner{T}"/> may result in performance loss.
/// </remarks>
public static IMemoryOwner<byte> DecompressToMemory(ReadOnlySpan<byte> input)
{
Expand All @@ -144,13 +158,40 @@ public static IMemoryOwner<byte> DecompressToMemory(ReadOnlySpan<byte> input)
return decompressor.ExtractData();
}

/// <summary>
/// Decompress a block of Snappy to a new memory buffer. This must be an entire block.
/// </summary>
/// <param name="input">Data to decompress.</param>
/// <returns>An <see cref="IMemoryOwner{T}"/> with the decompressed data. The caller is responsible for disposing this object.</returns>
/// <exception cref="InvalidDataException">Incomplete Snappy block.</exception>
/// <remarks>
/// Failing to dispose of the returned <see cref="IMemoryOwner{T}"/> may result in performance loss.
/// </remarks>
public static IMemoryOwner<byte> DecompressToMemory(ReadOnlySequence<byte> input)
{
using var decompressor = new SnappyDecompressor();

foreach (ReadOnlyMemory<byte> segment in input)
{
decompressor.Decompress(segment.Span);
}

if (!decompressor.AllDataDecompressed)
{
ThrowHelper.ThrowInvalidDataException("Incomplete Snappy block.");
}

return decompressor.ExtractData();
}

/// <summary>
/// Decompress a block of Snappy to a new byte array. This must be an entire block.
/// </summary>
/// <param name="input">Data to decompress.</param>
/// <returns>The decompressed data.</returns>
/// <exception cref="InvalidDataException">Invalid Snappy block.</exception>
/// <remarks>
/// The resulting byte array is allocated on the heap. If possible, <see cref="DecompressToMemory"/> should
/// The resulting byte array is allocated on the heap. If possible, <see cref="DecompressToMemory(ReadOnlySpan{byte})"/> should
/// be used instead since it uses a shared buffer pool.
/// </remarks>
public static byte[] DecompressToArray(ReadOnlySpan<byte> input)
Expand Down

0 comments on commit 5a01bad

Please sign in to comment.