Не позволяйте GZipStream/DeflateStream потреблять больше, чем сжатые данные

У меня есть файл, который можно было создать примерно так:

stream.Write(headerBytes, 0, headerBytes.Count);

using (var gz = new GZipStream(stream, Compress, leaveOpen: true);
{
    gz.Write(otherBytes, 0, otherBytes.Count);
}

stream.Write(moreBytes, 0, moreBytes.Count);

Теперь при чтении файла типа

stream.Read(headerBytes, 0, headerBytes.Count);
// in reality I make sure that indeed headerBytes.Count get read,
// something the above line omits

using (var gz = new GZipStream(stream, Decompress, leaveOpen: true)
{
  do { /* use buffer... */}
  while ((bytesRead = gz.Read(buffer, 0, buffer.Length)) != 0);
}

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
  // use buffer...

Получается, что GZipStream (то же самое верно и для DeflateStream) читает 16384 байта из stream вместо фактических 13293 сжатых байтов в случае, который я проверял.

Предполагая, что я заранее не знаю ни размер сжатой части файла, ни количество байтов, следующих за сжатыми данными, есть ли способ использовать GzipStream/DeflateStream

  1. поэтому он читает только сжатые данные из stream
  2. или хотя бы выяснить, каков был размер части сжатых данных, чтобы я мог stream.Position -= actuallyRead - compressedSize вручную?

person Evgeniy Berezovsky    schedule 11.03.2015    source источник


Ответы (3)


Этот интерфейс, по-видимому, не предоставляет средств для выполнения того, что вы хотите, что является одной из многих причин не использовать .NET. GZipStream или DeflateStream.

Вместо этого следует использовать DotNetZip.

person Mark Adler    schedule 11.03.2015

Этот ответ представляет собой некрасивый обходной путь. Мне это не особенно нравится, но это работает (за исключением случаев, когда это не так), даже если только для GZipStream.

  1. или хотя бы выяснить, каков был размер части сжатых данных, чтобы я мог stream.Position -= actuallyRead - compressedSize вручную?

Поскольку каждый файл gzip (и фактически каждый член gzip) оканчивается на

     +---+---+---+---+---+---+---+---+
     |     CRC32     |     ISIZE     |
     +---+---+---+---+---+---+---+---+

     CRC32
        This contains a Cyclic Redundancy Check value of the
        uncompressed data

     ISIZE
        This contains the size of the original (uncompressed) input
        data modulo 2^32.

Я мог бы просто использовать несжатый размер (модуль 2 ^ 32), который я знаю после закрытия GzipStream, и искать в потоке назад, пока не найду эти 4 байта, соответствующие ему.

Чтобы сделать его более надежным, я также должен вычислить CRC32 при распаковке и искать в потоке назад сразу после 8 байтов, формирующих правильный CRC32 и ISIZE.

Некрасиво, но я предупреждал.

‹сарказм>Как же я люблю инкапсуляцию. Инкапсуляция всего полезного, оставляя нам декомпрессирующий Stream, который работает именно в том случае, который предвидел всезнающий разработчик API.‹/sarcasm>

Вот быстрая реализация SeekBack, которая пока работает:

/// <returns>the number of bytes sought back (including bytes.Length)
///          or 0 in case of failure</returns>
static int SeekBack(Stream s, byte[] bytes, int maxSeekBack)
{
    if (maxSeekBack != -1 && maxSeekBack < bytes.Length)
        throw new ArgumentException("maxSeekBack must be >= bytes.Length");

    int soughtBack = 0;
    for (int i = bytes.Length - 1; i >= 0; i--)
    {
        while ((maxSeekBack == -1 || soughtBack < maxSeekBack)
               && s.Position > i)
        {
            s.Position -= 1;
            // as we are seeking back, the following will never become
            // -1 (EOS), so coercing to byte is OK
            byte b = (byte)s.ReadByte();
            s.Position -= 1;
            soughtBack++;
            if (b == bytes[i])
            {
                if (i == 0)
                    return soughtBack;
                break;
            }
            else
            {
                var bytesIn = (bytes.Length - 1) - i;
                if (bytesIn > 0) // back to square one
                {
                    soughtBack -= bytesIn;
                    s.Position += bytesIn;
                    i = bytes.Length - 1;
                }
            }
        }
    }
    // no luck? return to original position
    s.Position += soughtBack;
    return 0;
}
person Evgeniy Berezovsky    schedule 11.03.2015

Следуя предложению Марка Адлера, я попробовал DotNetZip, и, о чудо, его свойство GZipStream.Position не только не бросает, но и даже возвращает количество фактически прочитанных байтов gzip (плюс 8, по какой-то причине, которую мне еще предстоит выяснить).

Таким образом, он читает больше, чем необходимо, но позволяет мне рассчитать, на сколько нужно вернуться.

Для меня работает следующее:

var posBefore = fileStream.Position;
long compressedBytesRead;
using (var gz = new GZipStream(fileStream, CompressionMode.Decompress, true))
{
    while (gz.Read(buffer, 0, buffer.Length) != 0)
        ; // use it!
    compressedBytesRead = gz.Position;
}
var gzipStreamAdvance = fileStream.Position - posBefore;
var seekBack = gzipStreamAdvance - compressedBytesRead - 8; // but why "- 8"?
fileStream.Position -= seekBack;
person Evgeniy Berezovsky    schedule 13.03.2015