4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】 圧縮ストリームの読み取りに苦戦した話

Posted at

はじめに

System.IO.Compression.ZipArchive などの圧縮ストリームRead() メソッドは、1回でバッファを読み取りきれないことがあります。

var want = (stackalloc byte[8192]);
// 乱数で適当なデータを生成
new Random(0).NextBytes(want);

using var stream = new MemoryStream();
using (var writeZip = new ZipArchive(stream, ZipArchiveMode.Create, true))
{
    var entry = writeZip.CreateEntry("data");
    using var entryStream = entry.Open();
    entryStream.Write(want);
}

stream.Position = 0;

var got = (stackalloc byte[want.Length]);
using (var readZip = new ZipArchive(stream, ZipArchiveMode.Read))
{
    var entry = readZip.GetEntry("data");
    using var entryStream = entry!.Open();
    // Read() の戻り値は読み取ったバイト数
    // ストリームの終わりに達した場合は 0 が返る
    // 途中でストリームの終わりに達した場合は、読み取ったバイト数が返る
    // ストリームの種類によっては読み取りの途中でもバッファ長以外の数値が返る
    _ = entryStream.Read(got);
}

// データが一致しないことを確認
Assert.False(want.SequenceEqual(got));

サンプルコード

サンプルコード
using System.IO.Compression;
using Xunit;

public class _CompressionStreamReadTest
{
    /// <summary>
    /// ストリームからバッファを読み取る<br/>
    /// バッファの長さ分を読み取るまで反復的に読み取る
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="buffer"></param>
    /// <returns></returns>
    public static int ReadBuffer(Stream stream, Span<byte> buffer)
    {
        var read = stream.Read(buffer);
        if (read <= 0 || read == buffer.Length)
            return read;

        var written = read;
        for (int n = 0; n < 10; ++n)
        {
            var buffer2 = buffer[written..];
            read = stream.Read(buffer2);
            if (read <= 0)
                return written;

            written += read;
            if (written == buffer.Length)
                return written;
        }

        return written;
    }

    [Fact]
    void CompressionRead()
    {
        var want = (stackalloc byte[8192]);
        // 乱数で適当なデータを生成
        new Random(0).NextBytes(want);

        using var stream = new MemoryStream();
        using (var writeZip = new ZipArchive(stream, ZipArchiveMode.Create, true))
        {
            var entry = writeZip.CreateEntry("data");
            using var entryStream = entry.Open();
            entryStream.Write(want);
        }

        stream.Position = 0;

        var got = (stackalloc byte[want.Length]);
        using (var readZip = new ZipArchive(stream, ZipArchiveMode.Read))
        {
            var entry = readZip.GetEntry("data");
            using var entryStream = entry!.Open();
            _ = entryStream.Read(got);
        }

        // データが一致しないことを確認
        Assert.False(want.SequenceEqual(got));
    }

    [Fact]
    void CompressionReadBuffer()
    {
        var want = (stackalloc byte[8192]);
        // 乱数で適当なデータを生成
        new Random(0).NextBytes(want);

        using var stream = new MemoryStream();
        using (var writeZip = new ZipArchive(stream, ZipArchiveMode.Create, true))
        {
            var entry = writeZip.CreateEntry("data");
            using var entryStream = entry.Open();
            entryStream.Write(want);
        }

        stream.Position = 0;

        var got = (stackalloc byte[want.Length]);
        using (var readZip = new ZipArchive(stream, ZipArchiveMode.Read))
        {
            var entry = readZip.GetEntry("data");
            using var entryStream = entry!.Open();
            ReadBuffer(entryStream, got);
        }

        Assert.Equal(want, got);
    }
}

ストリームからバッファを読み取るコード

/// <summary>
/// ストリームからバッファを読み取る<br/>
/// バッファの長さ分を読み取るまで反復的に読み取る
/// </summary>
/// <param name="stream"></param>
/// <param name="buffer"></param>
/// <returns></returns>
public static int ReadBuffer(this Stream stream, Span<byte> buffer)
{
    var read = stream.Read(buffer);
    if (read <= 0 || read == buffer.Length)
        return read;

    var written = read;
    for (int n = 0; n < 10; ++n)
    {
        var buffer2 = buffer[written..];
        read = stream.Read(buffer2);
        if (read <= 0)
            return written;

        written += read;
        if (written == buffer.Length)
            return written;
    }

    return written;
}

指定したバッファ分を全部読み取ると思い込んでいた

// ストリームの種類によっては読み取りの途中でもバッファ長以外の数値が返る

これが肝要です。普段テキスト等をストリームから読み取るとき、ストリームから直接バイトを取得するのではなくストリームリーダーに任せることが多く、詳細な挙動を把握していませんでした。
一応 Read() に関するヒントは存在し、

// 戻り値を読み取っていないと警告が出る
// 'System.IO.Stream.Read(System.Span<byte>)' による不正確な読み取りを避ける(CA2022)
stream.Read(buffer);

これを見て今回の解決方法にたどり着きました。

.NET Framework 時代と挙動が変わった気がする

自分の記憶の限り .NET Framework 時代はバッファを全部読み取る挙動だったと思います。.NET に移行するときにパフォーマンス向上等の理由で仕様が変更されたのかもしれません。
このことに気づくまで MemoryStream にコピーを経由して読み取る荒業でごまかしていました。MemoryStream だと、Read() 1回で全部読み取ってくれます。

おわりに

今回この挙動に5年越しに気づきました。先送りにしていた謎のバグが解消して、大変よろしいです。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?