5

次のようなコードで作成されたファイルがあります。

        using (var fs=File.OpenWrite("tmp"))
        {
            using (GZipStream gs=new GZipStream(fs,CompressionMode.Compress,true))
            {
                using (StreamWriter sw=new StreamWriter(gs))
                {
                    sw.WriteLine("hello ");
                }
            }

            using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress, true))
            {
                using (StreamWriter sw = new StreamWriter(gs))
                {
                    sw.WriteLine("world");
                }
            }
        }

今、次のコードでこのファイルからデータを読み込もうとしています:

        string txt;

        using (var fs=File.OpenRead("tmp"))
        {
            using (GZipStream gs=new GZipStream(fs,CompressionMode.Decompress,true))
            {
                using (var rdr = new StreamReader(gs))
                {
                    txt = rdr.ReadToEnd();
                }
            }

            using (GZipStream gs = new GZipStream(fs, CompressionMode.Decompress, true))
            {
                using (StreamReader sr = new StreamReader(gs))
                {
                    txt+=sr.ReadToEnd();
                }
            }
        }

最初のストリームは問題なく読み取れますが、2 番目のストリームは読み取れません。

2 番目のストリームを読み取るにはどうすればよいですか?

4

4 に答える 4

5

これは、GzipStream が複数の gzip エントリを持つ gzip ファイルを処理する方法に問題があります。最初のエントリを読み取り、後続のすべてのエントリをガベージとして扱います (興味深いことに、gzip や winzip などのユーティリティは、それらすべてを 1 つのファイルに抽出することで正しく処理します)。回避策がいくつかあります。 DotNetZip ( http://dotnetzip.codeplex.com/ )。

おそらく最も簡単な方法は、すべての gzip ヘッダーのファイルをスキャンしてから、手動でストリームを各ヘッダーに移動し、コンテンツを解凍することです。これは、未加工のファイル バイトで ID1、ID2、および 0x8 を検索することで実行できます (Deflate 圧縮方法については、仕様を参照してください: http://www.gzip.org/zlib/rfc-gzip.html )。これは、gzip ヘッダーを見ていることを保証するには必ずしも十分ではないため、ヘッダーの残り (または少なくとも最初の 10 バイト) を読み取って確認する必要があります。

    const int Id1 = 0x1F;
    const int Id2 = 0x8B;
    const int DeflateCompression = 0x8;
    const int GzipFooterLength = 8;
    const int MaxGzipFlag = 32; 

    /// <summary>
    /// Returns true if the stream could be a valid gzip header at the current position.
    /// </summary>
    /// <param name="stream">The stream to check.</param>
    /// <returns>Returns true if the stream could be a valid gzip header at the current position.</returns>
    public static bool IsHeaderCandidate(Stream stream)
    {
        // Read the first ten bytes of the stream
        byte[] header = new byte[10];

        int bytesRead = stream.Read(header, 0, header.Length);
        stream.Seek(-bytesRead, SeekOrigin.Current);

        if (bytesRead < header.Length)
        {
            return false;
        }

        // Check the id tokens and compression algorithm
        if (header[0] != Id1 || header[1] != Id2 || header[2] != DeflateCompression)
        {
            return false;
        }

        // Extract the GZIP flags, of which only 5 are allowed (2 pow. 5 = 32)
        if (header[3] > MaxGzipFlag)
        {
            return false;
        }

        // Check the extra compression flags, which is either 2 or 4 with the Deflate algorithm
        if (header[8] != 0x0 && header[8] != 0x2 && header[8] != 0x4)
        {
            return false;
        }

        return true;
    }

ファイル ストリームを直接使用する場合、GzipStream はストリームをファイルの末尾に移動する可能性があることに注意してください。各部分を MemoryStream に読み込んでから、各部分をメモリ内で個別に解凍することができます。

別のアプローチは、gzip ヘッダーを変更してコンテンツの長さを指定することです。これにより、ファイルをスキャンしてヘッダーを探す必要がなくなります (それぞれのオフセットをプログラムで決定できます)。 gzip 仕様。

于 2013-03-09T08:59:26.550 に答える
5

マルチパート gzip 処理は、現在 .NET Core に実装されているようです。この議論は、.NET Framework に対しても有効です。


これは GzipStream のバグです。gzip 形式のRFC 1952 仕様に従って:

2.2. ファイル形式

gzip ファイルは、一連の「メンバー」(圧縮されたデータ セット) で構成されます。各メンバーの形式は、次のセクションで指定します。メンバーは、ファイル内で次々に出現するだけで、メンバーの前、間、後に追加情報はありません。

そのため、前の gzip メンバーの直後に別の gzip メンバーを探すには、準拠した解凍プログラムが必要です。

バグのある GzipStream を使用して単一の gzip メンバーを読み取り、次に GzipStream を再度使用して、GzipStream の最後の使用で使用されなかった最初の入力バイトから始まる次の gzip メンバーを読み取る単純なループを持つことができるはずです。gzip メンバーの先頭を検索しようとする他の提案とは対照的に、それは完全に信頼できます。

圧縮データには任意のバイト パターンを含めることができるため、実際には gzip メンバーの圧縮データの一部であるにもかかわらず、gzip ヘッダーを見つけたと誤解される可能性があります。実際、deflate メソッドの 1 つは圧縮せずにデータを保存することです。この場合、gzip メンバー内で圧縮された gzip ストリームが保存される可能性があります (データの大部分が圧縮されているため、それ以上圧縮できない可能性が非常に高いため)。そのため、gzip メンバーの圧縮データの途中で、完全に有効な偽の gzip ヘッダーが表示されます。

代わりに DotNetZip を使用するという提案は優れたものです。GzipStream には多くのバグがあり、NET 4.5 で修正されたものもあれば、明らかに修正されていないものもあります。Microsoft がそのクラスを正しく作成する方法を理解するには、さらに数年かかる可能性があります。DotNetZip は機能します。

于 2013-03-09T18:42:26.317 に答える
2

DeflateStream でも同様の問題がありました。

簡単なアプローチは、Read(byte[] buffer, int offset, int count) の呼び出しが行われたときに単一バイトのみを返す Stream 実装で、基になる Stream をラップすることです。これにより、DeflateStream/GZipStream のバッファリングが妨げられ、最初のストリームの最後に到達したときに、基になるストリームが正しい位置に残されます。もちろん、ここでは Read の呼び出し数が増えるため、明らかに非効率的ですが、アプリケーションによっては問題にならない場合があります。

DeflateStream の内部を調べてみると、リフレクションを使用して内部 Inflater インスタンスをリセットできる可能性があります。

于 2014-02-27T08:25:36.107 に答える