• Blog
  • Categories
    • App Store
    • Benchmarks
    • iOS
    • Objective-C
    • OS X
    • PHP
    • RV
    • Swift
    • tvOS
    • Web
  • Apps
    • Portfolio
    • Studio Pro
    • Sun & Moon
  • Photography

Projects

Remote Working, iOS, Mac OS X, and more

PHP

Edge Cases – gzdecode

PHP is a language that has largely had its development driven by the real world use and demands by its users. Those users, unfortunately, have not always updated in a timely fashion, so functionality that has been implemented over multiple versions is not always fully available and can create problems for the unaware.

The zlib family of functions are one such animal. Despite it technically being an optional library, it’s virtually always included and most of the functions have been available since PHP 4. There’s a notable exception though: gzdecode, the complement for gzencode. It was not added into PHP’s zlib integration with the rest 1 — it came much later with PHP 5.4.

Workaround

Fortunately, it’s possible to achieve the functionality of gzdecode with gzinflate:

if (!function_exists('gzdecode'))
{
	function gzdecode($data, $length = 0) //Based on https://www.ietf.org/rfc/rfc1952.txt
	{
		//Ensure the data meets the minimum length for header, checksum, and footer
		$length = strlen($data);
		if ($length < 18) { return null; }
	
		$header_length = 10;
		extract(unpack('nmagic_bytes/Cmethod/Cflags/Vmtime/Cxfl/Cos', $data));
	
		if ($magic_bytes !== 0x1f8b) { return null; } //Ensure it starts with GZIP's magic bytes
		if ($method !== 8) { return null; } //Only deflate is supported
		if (($flags & 0x1f) !== $flags) { return null; } //Reserved bits should not be set
	
		$extra_length = 0;
		$extra = '';
		if ($flags & 4) //FEXTRA is set, 2-byte length + data
		{
			if ($length - $header_length - 2 < 8) { return null; }
			extract(unpack('vextra_length', substr($data, 8, 2)));
			if ($length - $header_length - 2 - $extra_length < 8) { return null; }
			$extra = substr($data, 10, $extra_length);
			$header_length += 2 + $extra_length;
		}
	
		$filename_length = 0;
		$filename = '';
		if ($flags & 8) //FNAME is set, null-terminated C-string
		{
			if ($length - $header_length - 1 < 8) { return null; }
			$filename_length = strpos(substr($data, $header_length), chr(0));
			if ($filename_length === false || $length - $header_length - $filename_length - 1 < 8) { return null; }
			$filename = substr($data, $header_length, $filename_length);
			$header_length += $filename_length + 1;
		}
	
		$comment_length = 0;
		$comment = '';
		if ($flags & 16) //FCOMMENT is set, null-terminated C-string
		{
			if ($length - $header_length - 1 < 8) { return null; }
			$comment_length = strpos(substr($data, $header_length), chr(0));
			if ($comment_length === false || $length - $header_length - $comment_length - 1 < 8) { return null; }
			$comment = substr($data, $header_length, $comment_length);
			$header_length += $comment_length + 1;
		}
	
		$header_crc16 = '';
		if ($flags & 2) //FHCRC is set, 2-byte CRC16
		{
			/*
				RFC:
				If FHCRC is set, a CRC16 for the gzip header is present,
				immediately before the compressed data. The CRC16 consists
				of the two least significant bytes of the CRC32 for all
				bytes of the gzip header up to and not including the CRC16.
			*/
			if ($length - $header_length - 2 < 8) { return null; }
			$crc16 = crc32(substr($data, 0, $header_length)) & 0xffff;
			extract(unpack('vheader_crc16', substr($data, $header_length, 2)));
			if ($header_crc16 !== $crc16) { return null; }
			$header_length += 2;
		}
	
		//Decompress
		$body_length = $length - $header_length - 8;
		if ($body_length < 0) { return null; }
		$body = substr($data, $header_length, $body_length);
		$decompressed_body = '';
		if ($body_length > 0)
		{
			$decompressed_body = gzinflate($body, $length);
			if ($decompressed_body === false) { return null; }
		}
	
		//Verify CRC32
		extract(unpack('Vdata_crc32/Visize', substr($data, -8)));
		$crc32 = sprintf("%u", crc32($decompressed_body)); //From docs: Because PHP's integer type is signed many crc32 checksums will result in negative integers on 32bit platforms. On 64bit installations all crc32() results will be positive integers though. So you need to use the "%u" formatter of sprintf() or printf() to get the string representation of the unsigned crc32() checksum in decimal format.
		if ($crc32 != $data_crc32 || strlen($decompressed_body) != $isize) { return null; }
		return $decompressed_body;
	}
}
Previous
Next

Copyright 2025 Ryan Britton