How do I use Zlib with concatenated .gz files in winAPI?

0

I am downloading common crawl files from AWS. Apparently, they are large concatenated .gz files, which is supported by the gzip standard. I am using zlib to deflate but I only get the decompressed contents of the file up to the first concatenation. I have tried adding inflateReset() but then I get error -5, which indicates a buffer or file problem. I suspect I am close.

here's the code without inflateReset. It works fine on non-concatenated files.

#include "zlib.h"  
#define CHUNK 16384   
...
file = L"CC-MAIN-20181209185547-20181209211547-00040.warc.wet.gz";
fileDecompress(&file);

DWORD WINAPI fileDecompress(LPVOID lpParameter)
{
wstring dir = L"C:\\AI\\corpora\\";
wstring* lpFileName = static_cast<wstring*>(lpParameter);
sendToReportWindow(L"File to decompress is \"%s\" in \"%s\"\n", lpFileName->c_str(), dir.c_str());
wstring sourcePath = dir + lpFileName->c_str();
sendToReportWindow(L"input file with path:%s\n", sourcePath.c_str());
wstring destPath = dir + lpFileName->c_str() + L".wet";
sendToReportWindow(L"output file with path:%s\n", destPath.c_str());

HANDLE InputFile = INVALID_HANDLE_VALUE;
HANDLE OutputFile = INVALID_HANDLE_VALUE;
BOOL Success;
DWORD InputFileSize;
ULONGLONG StartTime, EndTime;
LARGE_INTEGER FileSize;

//  Open input file for reading, existing file only.
InputFile = CreateFile(
    sourcePath.c_str(),       //  Input file name, compressed file
    GENERIC_READ,             //  Open for reading
    FILE_SHARE_READ,          //  Share for read
    NULL,                     //  Default security
    OPEN_EXISTING,            //  Existing file only
    FILE_ATTRIBUTE_NORMAL,    //  Normal file
    NULL);                    //  No template

if (InputFile == INVALID_HANDLE_VALUE)
{
    sendToReportWindow(L"Cannot open input \t%s\n", sourcePath.c_str());
    return 0;
}

OutputFile = CreateFile(
    destPath.c_str(),         //  Input file name, compressed file
    GENERIC_WRITE,            //  Open for reading
    0,                        //  Share for read
    NULL,                     //  Default security
    CREATE_ALWAYS,            //  Existing file only
    FILE_ATTRIBUTE_NORMAL,    //  Normal file
    NULL);                    //  No template

if (OutputFile == INVALID_HANDLE_VALUE)
{
    sendToReportWindow(L"Cannot open output \t%s\n", destPath.c_str());
    return 0;
}

//  Get compressed file size.
Success = GetFileSizeEx(InputFile, &FileSize);
if ((!Success) || (FileSize.QuadPart > 0xFFFFFFFF))
{
    sendToReportWindow(L"Cannot get input file size or file is larger than 4GB.\n");
    CloseHandle(InputFile);
    return 0;
}
InputFileSize = FileSize.LowPart;

sendToReportWindow(L"input file size: %u bytes\n", InputFileSize);

int ret;
unsigned have;
z_stream strm;
unsigned char in[CHUNK];
unsigned char out[CHUNK];

strm.zalloc = Z_NULL;              // allocate inflate state
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;

ret = inflateInit2(&strm, 16 + MAX_WBITS);
if (ret != Z_OK)
{
return 0;
}

do {                                                                    /* decompress until deflate stream ends or end of file */  
    DWORD read;
    BOOL res = ReadFile(InputFile, in, CHUNK, &read, NULL);

    strm.avail_in = read;
    if (!res) {
        (void)inflateEnd(&strm);
        sendToReportWindow(L"read error on input file\n");
        return 0;
    }

    if (strm.avail_in == 0)
    {
        break;
    }
    strm.next_in = in;


        /* run inflate() on input until output buffer not full */
    do {
        strm.avail_out = CHUNK;
        strm.next_out = out;
        ret = inflate(&strm, Z_NO_FLUSH);

        assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
        switch (ret) {
        case Z_NEED_DICT:                                           // 2
            sendToReportWindow(L"z_need_dict:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
            //ret = Z_DATA_ERROR;     /* and fall through */
        case Z_DATA_ERROR:                                          // -3
            sendToReportWindow(L"z_data_error:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
        case Z_MEM_ERROR:                                           // -4
            (void)inflateEnd(&strm);
            sendToReportWindow(L"z_mem_error:%d\n", ret);
            sendToReportWindow(L"ret:%d\n", ret);
            DisplayErrorBox((LPWSTR)L"inflate");
            return 0;
        case Z_BUF_ERROR:                                           // -5
            sendToReportWindow(L"z_buf_error:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
        }

        have = CHUNK - strm.avail_out;   
        DWORD written;
        BOOL res = WriteFile(OutputFile, out, have, &written, NULL);

        if (written != have || !res) {
            (void)inflateEnd(&strm);
            sendToReportWindow(L"file write error:%d\n", res);
            return 0;
        }
 
    } while (strm.avail_out == 0);          //  avail_out == 0 means output buffer is full 
} while (ret != Z_STREAM_END);  /* done when inflate() says it's done */            // Z_STREAM_END is 1

(void)inflateEnd(&strm);
CloseHandle(InputFile); CloseHandle(OutputFile);
return 0;
}

Here's the version with the inflateReset() added. this version causes inflate to generate error -5 (bad buffer or truncated file).

...
int ret;
z_stream strm{};
array<uint8_t, CHUNK> scratch = {}; //scratch buffer for decompressing the data.

strm.zalloc = Z_NULL;              // allocate inflate state
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;

ret = inflateInit2(&strm, 16 + MAX_WBITS);
if (ret != Z_OK)
{
    return 0;
}

do {                                                                    /* decompress until deflate stream ends or end of file */ 
    DWORD read;
    BOOL res = ReadFile(InputFile, in, CHUNK, &read, NULL);

    strm.avail_in = read;
    if (!res) {
        (void)inflateEnd(&strm);
        sendToReportWindow(L"read error on input file\n");
        return 0;
    }

    if (strm.avail_in == 0)
    {
        sendToReportWindow(L"strm.avail_in:%d\n", strm.avail_in);       // strm.avail_in = 0
        break;
    }
    strm.next_in = in;

        /* run inflate() on input until output buffer not full */
    do {
        strm.avail_out = scratch.size();
        strm.next_out = scratch.data();
        ret = inflate(&strm, Z_NO_FLUSH);

        //if (ret != Z_OK) break;                                     // 0
        
        switch (ret) {
        case Z_NEED_DICT:                                           // 2
            sendToReportWindow(L"z_need_dict:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
            //ret = Z_DATA_ERROR;     /* and fall through */
        case Z_STREAM_ERROR:                                        // -2
            sendToReportWindow(L"Z_STREAM_ERROR:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
        case Z_DATA_ERROR:                                          // -3
            sendToReportWindow(L"z_data_error:%d\n", ret);
            (void)inflateEnd(&strm);
            return 0;
        case Z_MEM_ERROR:                                           // -4
            (void)inflateEnd(&strm);
            sendToReportWindow(L"z_mem_error:%d\n", ret);
            sendToReportWindow(L"ret:%d\n", ret);
            DisplayErrorBox((LPWSTR)L"inflate");
            return 0;
        case Z_BUF_ERROR:                                           // -5
            sendToReportWindow(L"z_buf_error:%d\n", ret);
            (void)inflateEnd(&strm);
            //return 0;
            break;
        }

        auto bytes_decoded = scratch.size() - strm.avail_out;
       
        DWORD written;
        BOOL res = WriteFile(OutputFile, &scratch, bytes_decoded, &written, NULL);

        if (ret == Z_STREAM_END) break;

    } while (true);          //  avail_out == 0 means output buffer is full

    ret == Z_STREAM_END;

    auto reset_result = inflateReset(&strm);        // work with concatenation
    sendToReportWindow(L"resetting inflate: %d\n", reset_result);
    assert(reset_result == Z_OK);      

} while (strm.avail_in > 0);
...

Thank you!

update: I think readFile should read in CHUNK instead of 1. changed for both examples. This now gives me error -3: "Z_DATA_ERROR". checking to see if this change is now actually hitting readfile too many times.

typical file I want to deflate: [https://commoncrawl.s3.amazonaws.com/crawl-data/CC-MAIN-2018-51/segments/1544376823009.19/wet/CC-MAIN-20181209185547-20181209211547-00041.warc.wet.gz]

update 2: Thank you Mark Adler! using the example you provided, I was able to fix the logic in my code. this satisfies the winAPI requirement. I also added file ext handling, moved things to heap and added a timer. The timer revealed that more memory helped reduce deflate time by 30%.

DWORD WINAPI fileDecompress(LPVOID lpParameter)
{                                                                                
// zlib does not work with .zip files
sendToReportWindow(L"inside fileDecompress()\n");                            
// deflate .gz (gzip) files. single or multiple member (concatenated)

wstring dir = L"C:\\AI\\corpora\\";
wstring* lpFileName = static_cast<wstring*>(lpParameter);
sendToReportWindow(L"File to decompress is \"%s\" in \"%s\"\n", lpFileName->c_str(), dir.c_str());
wstring sourcePath = dir + lpFileName->c_str();
sendToReportWindow(L"input file with path:%s\n", sourcePath.c_str());

wstring::size_type lastdot = lpFileName->find_last_of(L".");                 // remove .gz extension: get length to last dot and truncate
lpFileName->resize(lastdot);
wstring destPath = dir + lpFileName->c_str();
sendToReportWindow(L"output file with path:%s\n", destPath.c_str());

HANDLE InputFile = INVALID_HANDLE_VALUE;
HANDLE OutputFile = INVALID_HANDLE_VALUE;
BOOL Success;
DWORD InputFileSize;
ULONGLONG StartTime, EndTime;
LARGE_INTEGER FileSize;
double InflateTime;

InputFile = CreateFile(
    sourcePath.c_str(),       //  Input file name, compressed file
    GENERIC_READ,             //  Open for reading
    FILE_SHARE_READ,          //  Share for read
    NULL,                     //  Default security
    OPEN_EXISTING,            //  Existing file only
    FILE_ATTRIBUTE_NORMAL,    //  Normal file
    NULL);                    //  No template

if (InputFile == INVALID_HANDLE_VALUE){sendToReportWindow(L"Cannot open input \t%s\n", sourcePath.c_str()); return 0; }

OutputFile = CreateFile(
    destPath.c_str(),         //  Input file name, compressed file
    GENERIC_WRITE,            //  Open for reading
    0,                        //  Share for read
    NULL,                     //  Default security
    CREATE_ALWAYS,            //  Existing file only
    FILE_ATTRIBUTE_NORMAL,    //  Normal file
    NULL);                    //  No template

if (OutputFile == INVALID_HANDLE_VALUE){sendToReportWindow(L"Cannot open output \t%s\n", destPath.c_str()); return 0; }

Success = GetFileSizeEx(InputFile, &FileSize);                              // Get compressed file size.
if ((!Success) || (FileSize.QuadPart > 0xFFFFFFFF))
{
    sendToReportWindow(L"Cannot get input file size or file is larger than 4GB.\n");
    CloseHandle(InputFile);
    return 0;
}
InputFileSize = FileSize.LowPart;
sendToReportWindow(L"input file size: %u bytes\n", InputFileSize);

StartTime = GetTickCount64();

#define CHUNK 524288                                                        // buffer size. doesn't use much ram and speeds up inflate
z_stream strm = {};                                                         // Initialize zlib for file compression/decompression
int ret = inflateInit2(&strm, 16 + MAX_WBITS);
assert(ret == Z_OK);

unsigned char *in = new unsigned char[CHUNK]; unsigned char* out = new unsigned char[CHUNK];   

for (;;) {                                                                  // Decompress from input to output.
    if (strm.avail_in == 0) {                                               // Keep reading until the end of the input file or an error
        DWORD read;
        (void)ReadFile(InputFile, in, CHUNK, &read, NULL);
        strm.avail_in = read;
        if (strm.avail_in == 0)
            break;
        strm.next_in = in;
    }

    do {                                                                    // Decompress all of what's in the CHUNK in buffer.
        strm.avail_out = CHUNK;                                                     
        strm.next_out = out;
        ret = inflate(&strm, Z_NO_FLUSH);                                   // Decompress as much as possible to the CHUNK out buffer.
                                                                          
        size_t got = CHUNK - strm.avail_out;                                
        DWORD written;                                                      
        (void)WriteFile(OutputFile, out, got, &written, NULL);              // Write to the outputFile whatever inflate() left in out buffer
        if (written != got) {sendToReportWindow(L"file write error\n"); delete[] in; delete[] out; return 0;}
                                                                                                                      
        if (ret == Z_STREAM_END)                                            // Check for the end of a gzip member, in which case, 
            assert(inflateReset(&strm) == Z_OK);                            // reset inflate for the next gzip member. (concatenated files)

        else if (ret != Z_OK) {                                             // Return on a data error.
            assert(ret == Z_DATA_ERROR);
            (void)inflateEnd(&strm);
            delete[] in; delete[] out;
            return 0;
        }   
    } while (strm.avail_in > 0);                                            // Continue until everything in the input buffer is consumed.
}                                                                           // for() loop to get next input buffer CHUNK from input file    

EndTime = GetTickCount64();
InflateTime = (EndTime - StartTime) / 1000.0;                               //  Get how long it took to inflate file

delete[] in; delete[] out;
(void)inflateEnd(&strm);                                                       
CloseHandle(InputFile); CloseHandle(OutputFile);
sendToReportWindow(L"Inflate Time: %.2f seconds. Done with fileDecompress function.\n", InflateTime);
return 0;
}
c++
winapi
gzip
zlib
asked on Stack Overflow Sep 23, 2020 by kbaud • edited Sep 24, 2020 by kbaud

1 Answer

0

Does your compiler not at least warn you about the naked conditional ret == Z_STREAM_END;? You want an if there and some braces around the inflateReset() related statements.

There's still a problem in that you are leaving the outer loop if strm.avail_in is zero. That will happen every time, except when reaching the end of member. It can even happen then if you just so happen to exhaust the input buffer to decompress that member. Just make the outer loop a while (true).

Even after fixing all that, you would then discard the remaining available input when you do the read at the top of the outer loop. Only do that read if strm.avail_in is zero.

A simpler approach would be to do the reset in the inner loop. Like this (example in C):

// Decompress a gzip file input, potentially with multiple gzip members. Write
// the decompressed data to output. Return Z_STREAM_END on success. Return Z_OK
// if the gzip stream was correct up to where it ended prematurely. Return
// Z_DATA error if the gzip stream is invalid.
int inflate_gzip(FILE *input, FILE *output) {
    // Initialize inflate for gzip input.
    z_stream strm = {};
    int ret = inflateInit2(&strm, 16 + MAX_WBITS);
    assert(ret == Z_OK);

    // Decompress from input to output.
    unsigned char in[CHUNK];
    for (;;) {
        // Keep reading until the end of the input file or an error.
        if (strm.avail_in == 0) {
            strm.avail_in = fread(in, 1, CHUNK, input);
            if (strm.avail_in == 0)
                break;
            strm.next_in = in;
        }

        // Decompress all of what's in the input buffer.
        do {
            // Decompress as much as possible to the CHUNK output buffer.
            unsigned char out[CHUNK];
            strm.avail_out = CHUNK;
            strm.next_out = out;
            ret = inflate(&strm, Z_NO_FLUSH);

            // Write to the output file whatever inflate() left in the output
            // buffer. Return with an error if the write does not complete.
            size_t got = CHUNK - strm.avail_out;
            size_t put = fwrite(out, 1, got, output);
            if (put != got)
                return Z_ERRNO;

            // Check for the end of a gzip member, in which case reset inflate
            // for the next gzip member.
            if (ret == Z_STREAM_END)
                assert(inflateReset(&strm) == Z_OK);

            // Return on a data error.
            else if (ret != Z_OK) {
                assert(ret == Z_DATA_ERROR);
                (void)inflateEnd(&strm);
                return ret;
            }

            // Continue until everything in the input buffer is consumed.
        } while (strm.avail_in > 0);
    }

    // Successfully decompressed all of the input file. Clean up and return.
    assert(inflateEnd(&strm) == Z_OK);
    return ret;
}
answered on Stack Overflow Sep 23, 2020 by Mark Adler • edited Sep 23, 2020 by Mark Adler

User contributions licensed under CC BY-SA 3.0