这是gzip解压方法中的一个错误吗?

8
在搜索 iOS 上如何解压缩 gzip 压缩数据时,有许多结果中都出现了以下方法:
- (NSData *)gzipInflate
{
    if ([self length] == 0) return self;

    unsigned full_length = [self length];
    unsigned half_length = [self length] / 2;

    NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length];
    BOOL done = NO;
    int status;

    z_stream strm;
    strm.next_in = (Bytef *)[self bytes];
    strm.avail_in = [self length];
    strm.total_out = 0;
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;

    if (inflateInit2(&strm, (15+32)) != Z_OK) return nil;
    while (!done)
    {
        // Make sure we have enough room and reset the lengths.
        if (strm.total_out >= [decompressed length])
            [decompressed increaseLengthBy: half_length];
        strm.next_out = [decompressed mutableBytes] + strm.total_out;
        strm.avail_out = [decompressed length] - strm.total_out;

        // Inflate another chunk.
        status = inflate (&strm, Z_SYNC_FLUSH);
        if (status == Z_STREAM_END) done = YES;
        else if (status != Z_OK) break;
    }
    if (inflateEnd (&strm) != Z_OK) return nil;

    // Set real length.
    if (done)
    {
        [decompressed setLength: strm.total_out];
        return [NSData dataWithData: decompressed];
    }
    else return nil;
}

但是我遇到了一些数据的例子(在Linux机器上使用Python的gzip模块进行压缩),这个在iOS上运行的方法无法解压。具体情况如下:
在while循环的最后一次迭代中,inflate()返回Z_BUF_ERROR并退出循环。但是在循环之后调用的inflateEnd()返回Z_OK。代码随后假定由于inflate()从未返回Z_STREAM_END,因此解压失败并返回null。
根据这个页面http://www.zlib.net/zlib_faq.html#faq05,Z_BUF_ERROR不是致命错误,我的有限测试表明,如果inflateEnd()返回Z_OK,则数据成功解压缩,即使最后一次调用inflate()没有返回Z_OK。似乎inflateEnd()已经完成了最后一块数据的解压。
我对压缩和gzip的工作原理知之甚少,因此在不完全了解其功能的情况下,我犹豫是否更改此代码。 我希望了解该主题的专业人士可以阐明上述代码中潜在的逻辑缺陷,并提出修复方法。
另一种谷歌找到的方法似乎也存在同样的问题,可以在此处找到:https://github.com/nicklockwood/GZIP/blob/master/GZIP/NSData%2BGZIP.m 编辑: 所以,这是一个错误!现在,我们该如何修复它? 以下是我的尝试。 有人能够审核代码吗?
- (NSData *)gzipInflate
{
    if ([self length] == 0) return self;

    unsigned full_length = [self length];
    unsigned half_length = [self length] / 2;

    NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length];
    int status;

    z_stream strm;
    strm.next_in = (Bytef *)[self bytes];
    strm.avail_in = [self length];
    strm.total_out = 0;
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;

    if (inflateInit2(&strm, (15+32)) != Z_OK) return nil;

    do
    {
        // Make sure we have enough room and reset the lengths.
        if (strm.total_out >= [decompressed length])
            [decompressed increaseLengthBy: half_length];
        strm.next_out = [decompressed mutableBytes] + strm.total_out;
        strm.avail_out = [decompressed length] - strm.total_out;

        // Inflate another chunk.
        status = inflate (&strm, Z_SYNC_FLUSH);

        switch (status) {
            case Z_NEED_DICT:
                status = Z_DATA_ERROR;     /* and fall through */
            case Z_DATA_ERROR:
            case Z_MEM_ERROR:
            case Z_STREAM_ERROR:
                (void)inflateEnd(&strm);
                return nil;
        }
    } while (status != Z_STREAM_END);

    (void)inflateEnd (&strm);

    // Set real length.
    if (status == Z_STREAM_END)
    {
        [decompressed setLength: strm.total_out];
        return [NSData dataWithData: decompressed];
    }
    else return nil;
}

编辑2:

这是一个示例Xcode项目,说明了我遇到的问题。压缩发生在服务器端,并且数据在通过HTTP传输之前进行base64和url编码。我已经将url编码的base64字符串嵌入ViewController.m中。url解码、base64解码以及gzipInflate方法都在NSDataExtension.m中。

https://dl.dropboxusercontent.com/u/38893107/gzip/GZIPTEST.zip

这是由Python gzip库压缩的二进制文件:

https://dl.dropboxusercontent.com/u/38893107/gzip/binary.zip

这是通过HTTP传输的URL编码的Base64字符串: https://dl.dropboxusercontent.com/u/38893107/gzip/urlEncodedBase64.txt

如果gzip流不完整,尝试会进入无限循环。 - Mark Adler
顺便说一下,“binary.zip”不是一个zip文件,而是一个gzip文件。名称应该是“binary.gz”。 - Mark Adler
该URL解码为binary.zip(应该称为binary.gz),我在我的答案中提供的代码可以正确地将其解压缩为一个221213字节的文本文件。我没有查看你的代码来看看哪里出了问题 - 那是你的工作。 - Mark Adler
谢谢马克,你的帮助超出了我的预期。 - subjective-c
2个回答

8

是的,这是一个bug。

事实上,如果inflate()没有返回Z_STREAM_END,那么你还没有完成解压。 inflateEnd()返回Z_OK并不意味着什么,只是表示它得到了一个有效的状态并且能够释放内存。

因此,在你宣布成功之前,inflate()必须最终返回Z_STREAM_END。然而,Z_BUF_ERROR并不是放弃的理由。在这种情况下,你只需要使用更多的输入或输出空间再次调用inflate()即可。然后你就会得到Z_STREAM_END

来自zlib.h文档:

/* ...
Z_BUF_ERROR if no progress is possible or if there was not enough room in the
output buffer when Z_FINISH is used.  Note that Z_BUF_ERROR is not fatal, and
inflate() can be called again with more input and more output space to
continue decompressing.
... */

更新:

由于存在错误的代码,下面是正确的代码以实现所需的方法。该代码可以处理不完整的gzip流、连接的gzip流和非常大的gzip流。对于非常大的gzip流,在编译为64位可执行文件时,z_stream中的unsigned长度不够大。 NSUInteger是64位,而unsigned是32位。在这种情况下,您必须循环输入以将其馈送给inflate()

此示例仅在出现任何错误时返回nil。如果需要更复杂的错误处理,则在每个return nil;后的注释中指出错误的性质。

- (NSData *) gzipInflate
{
    z_stream strm;

    // Initialize input
    strm.next_in = (Bytef *)[self bytes];
    NSUInteger left = [self length];        // input left to decompress
    if (left == 0)
        return nil;                         // incomplete gzip stream

    // Create starting space for output (guess double the input size, will grow
    // if needed -- in an extreme case, could end up needing more than 1000
    // times the input size)
    NSUInteger space = left << 1;
    if (space < left)
        space = NSUIntegerMax;
    NSMutableData *decompressed = [NSMutableData dataWithLength: space];
    space = [decompressed length];

    // Initialize output
    strm.next_out = (Bytef *)[decompressed mutableBytes];
    NSUInteger have = 0;                    // output generated so far

    // Set up for gzip decoding
    strm.avail_in = 0;
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    int status = inflateInit2(&strm, (15+16));
    if (status != Z_OK)
        return nil;                         // out of memory

    // Decompress all of self
    do {
        // Allow for concatenated gzip streams (per RFC 1952)
        if (status == Z_STREAM_END)
            (void)inflateReset(&strm);

        // Provide input for inflate
        if (strm.avail_in == 0) {
            strm.avail_in = left > UINT_MAX ? UINT_MAX : (unsigned)left;
            left -= strm.avail_in;
        }

        // Decompress the available input
        do {
            // Allocate more output space if none left
            if (space == have) {
                // Double space, handle overflow
                space <<= 1;
                if (space < have) {
                    space = NSUIntegerMax;
                    if (space == have) {
                        // space was already maxed out!
                        (void)inflateEnd(&strm);
                        return nil;         // output exceeds integer size
                    }
                }

                // Increase space
                [decompressed setLength: space];
                space = [decompressed length];

                // Update output pointer (might have moved)
                strm.next_out = (Bytef *)[decompressed mutableBytes] + have;
            }

            // Provide output space for inflate
            strm.avail_out = space - have > UINT_MAX ? UINT_MAX :
                             (unsigned)(space - have);
            have += strm.avail_out;

            // Inflate and update the decompressed size
            status = inflate (&strm, Z_SYNC_FLUSH);
            have -= strm.avail_out;

            // Bail out if any errors
            if (status != Z_OK && status != Z_BUF_ERROR &&
                status != Z_STREAM_END) {
                (void)inflateEnd(&strm);
                return nil;                 // invalid gzip stream
            }

            // Repeat until all output is generated from provided input (note
            // that even if strm.avail_in is zero, there may still be pending
            // output -- we're not done until the output buffer isn't filled)
        } while (strm.avail_out == 0);

        // Continue until all input consumed
    } while (left || strm.avail_in);

    // Free the memory allocated by inflateInit2()
    (void)inflateEnd(&strm);

    // Verify that the input is a valid gzip stream
    if (status != Z_STREAM_END)
        return nil;                         // incomplete gzip stream

    // Set the actual length and return the decompressed data
    [decompressed setLength: have];
    return decompressed;
}

非常感谢您抽出时间重写这段代码。您不知道有多少网站将该函数作为在iOS上膨胀gzip的方法进行引用。 - subjective-c
这意味着gzip流不完整,正如在返回“nil”的注释中所指出的那样。 - Mark Adler
它使用Python的gzip模块在服务器上进行了压缩,然后进行了base64编码并通过http传输,因此可能会在传输过程中出现问题。但有趣的是,再进行一次迭代就会到达Z_STREAM_END,并且我似乎可以获取所有数据。 - subjective-c
你能提供一个gzip流未被正确解压的示例吗? - Mark Adler
当然,可以查看上面的问题的编辑2。感谢您对此的关注。您已经超出了预期! - subjective-c
显示剩余4条评论

2
是的,看起来像是一个bug。根据zlib网站上这个有注释的示例Z_BUF_ERROR只是一种指示,表示除非inflate()提供更多输入,否则没有更多输出,本身并不是异常中止inflate循环的原因。
事实上,链接的示例似乎正好处理了Z_BUF_ERROR,就像处理Z_OK一样。

只要您一次性传递所有输入数据,我就看不出有什么问题。示例中的外部循环是用于“流式传输”的,即如果avail_out为0,则尝试重新填充输入缓冲区并重试,直到返回Z_STREAM_END。由于您一次性传递了所有数据,并且没有任何可重新填充的内容,我认为您除了重试直到获得硬错误或Z_STREAM_END之外别无选择。 - Joachim Isaksson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接