The other day I was working with a web service that gzipped some of its replies. I needed something simple and robust that would gunzip an NSData
without writing a file. When a little googling did not turn up a palatable solution, I implemented one of my own. I have made the code available on GitHub, but I also wanted to write a blog post about it, as there does seem to be a lot of confusion about how to use zlib
to gunzip data.
Since the data I needed to gunzip would be held in an NSData
I decided that using a category on NSData
was “the way to go”. The header file NSData+IDZGunzip.h
looks like this:
#import <Foundation/Foundation.h> extern NSString* const IDZGunzipErrorDomain; @interface NSData (IDZGunzip) - (NSData*)gunzip:(NSError**)error; @end
There’s not much to comment on here. This file adds a new method to NSData
, gunzip
and defines a constant IDZGunzipErrorDomain
that will be used in error handling.
Ignoring error handling for the moment, the implementation file NSData+IDZGunzip.m
looks like this:
#import "NSData+IDZGunzip.h" #import <zlib.h> NSString* const IDZGunzipErrorDomain = @"com.iosdeveloperzone.IDZGunzip"; @implementation NSData (IDZGunzip) - (NSData*)gunzip:(NSError *__autoreleasing *)error { z_stream zStream; memset(&zStream, 0, sizeof(zStream)); inflateInit2(&zStream, 16); UInt32 nUncompressedBytes = *(UInt32*)(self.bytes + self.length - 4); NSMutableData* gunzippedData = [NSMutableData dataWithLength:nUncompressedBytes]; zStream.next_in = (Bytef*)self.bytes; zStream.avail_in = self.length; zStream.next_out = (Bytef*)gunzippedData.bytes; zStream.avail_out = gunzippedData.length; inflate(&zStream, Z_FINISH); inflateEnd(&zStream); return gunzippedData; } @end
Let’s take a closer look at this code. Lines 10-12 initialize the zlib
stream structure and library for decompression. The 16
is a “magic” value that instructs zlib
to expect gzip compressed data.
Lines 14-15 create an appropriately sized output buffer to receive the output data. The last 4 bytes of gzipped data contain the length of the uncompressed data as a little-endian 32-bit unsigned integer.
Lines 17-20 connect the input and output buffers to the stream.
Line 22 does the actual decompression. The Z_FINISH
argument tells the library that this is the final (and in our case only) input buffer to force it flush all its output.
Line 24 releases any memory used by the library.
Finally, line 26 returns the output to the caller.
When we add in some error handling things are a little bit more complex, but not too much:
#import "NSData+IDZGunzip.h" #import <zlib.h> NSString* const IDZGunzipErrorDomain = @"com.iosdeveloperzone.IDZGunzip"; @implementation NSData (IDZGunzip) - (NSData*)gunzip:(NSError *__autoreleasing *)error { /* * A minimal gzip header/trailer is 18 bytes long. * See: RFC 1952 http://www.gzip.org/zlib/rfc-gzip.html */ if(self.length < 18) { if(error) *error = [NSError errorWithDomain:IDZGunzipErrorDomain code:Z_DATA_ERROR userInfo:nil]; return nil; } z_stream zStream; memset(&zStream, 0, sizeof(zStream)); /* * 16 is a magic number that allows inflate to handle gzip * headers. */ int iResult = inflateInit2(&zStream, 16); if(iResult != Z_OK) { if(error) *error = [NSError errorWithDomain:IDZGunzipErrorDomain code:iResult userInfo:nil]; return nil; } /* * The last four bytes of a gzipped file/buffer contain the the number * of uncompressed bytes expressed as a 32-bit little endian unsigned integer. * See: RFC 1952 http://www.gzip.org/zlib/rfc-gzip.html */ UInt32 nUncompressedBytes = *(UInt32*)(self.bytes + self.length - 4); NSMutableData* gunzippedData = [NSMutableData dataWithLength:nUncompressedBytes]; zStream.next_in = (Bytef*)self.bytes; zStream.avail_in = self.length; zStream.next_out = (Bytef*)gunzippedData.bytes; zStream.avail_out = gunzippedData.length; iResult = inflate(&zStream, Z_FINISH); if(iResult != Z_STREAM_END) { if(error) *error = [NSError errorWithDomain:IDZGunzipErrorDomain code:iResult userInfo:nil]; gunzippedData = nil; } inflateEnd(&zStream); return gunzippedData; } @end
The finished code can be used as follows:
#import "NSData+IDZGunzip.h" ... // Assuming data holds valid gzipped data NSError* error = nil; // gunzip the data NSData* gunzippedData = [data gunzip:&error]; if(!gunzippedData) { // Handle error } else { // Success use gunzippedData }
Get the code from GitHub at this URL: https://github.com/iosdevzone/IDZGunzip
Pingback: .Zlib 副檔名解壓縮成 NSData – doSomeThing